Apéndice 4.2 Google Colab¶

Algoritmos de Mejoramiento de Imágenes usando Operaciones Morfológicas¶


Maestría en Inteligencia Artificial Aplicada¶

Tecnológico de Monterrey


Profesores¶

  • Profesor Titular: Dr. Gilberto Ochoa Ruiz
  • Profesora Asistente: MIP Ma. del Refugio Melendez Alfaro
  • Profesor Tutor: M. en C. Jose Angel Martinez Navarro

Team 13 - Integrantes¶

Nombre Matrícula
Javier Augusto Rebull Saucedo A01795838
Juan Carlos Pérez Nava A01795941
Luis Gerardo Sánchez Salazar A01232963
Oscar Enrique García García A01016093

Información del Proyecto¶

  • Curso: Visión Computacional para Imágenes y Video
  • Actividad: Mini Proyecto - Detección de Placas Vehiculares
  • Fecha de Entrega: Octubre 5, 2025
  • Modalidad: Equipo

Descripción¶

Este notebook presenta un apéndice práctico de la Actividad 4.2, donde se implementa un pipeline completo de detección automática de placas vehiculares utilizando todas las operaciones morfológicas estudiadas en el curso: erosión, dilatación, apertura, cierre, gradiente morfológico y top-hat.


Apéndice: Aplicación Práctica de Operaciones Morfológicas¶

Mini Proyecto - Detección y Extracción de Placas Vehiculares¶


Descripción¶

Este apéndice presenta una aplicación práctica integral de las operaciones morfológicas estudiadas en la Actividad 4.2, implementando un pipeline completo de detección automática de placas vehiculares que utiliza todas las técnicas morfológicas del curso principal.


Estructura del Notebook¶

CELDA 1: Instalación de Librerías e Imports¶

Propósito: Configuración inicial del entorno de trabajo

  • Instala las librerías necesarias (OpenCV, rawpy, pillow-heif, gdown)
  • Importa todas las dependencias para procesamiento de imágenes
  • Verifica la ubicación geográfica y timestamp de ejecución
  • Inicializa el cronómetro del notebook

Operaciones Morfológicas: Ninguna (configuración)


CELDA 2: Descarga de Imágenes desde Google Drive¶

Propósito: Obtención del dataset de placas vehiculares

  • Define el inventario de imágenes con IDs de Google Drive
  • Genera mapeo secuencial de nombres (Plate 00, Plate 01, etc.)
  • Descarga automáticamente todas las imágenes
  • Crea archivo CSV con el mapping de nombres originales a nuevos

Operaciones Morfológicas: Ninguna (adquisición de datos)


CELDA 3: Procesamiento y Redimensión de Imágenes¶

Propósito: Preparación inicial del dataset

  • Lee archivos en múltiples formatos (JPG, HEIC, DNG, RAW)
  • Redimensiona imágenes a ancho máximo de 800px
  • Genera versiones en BGR, RGB, escala de grises y binarias
  • Almacena todas las versiones en listas separadas

Operaciones Morfológicas:

  • Binarización con umbral de Otsu (preparación para morfología)

CELDA 4: Visualización de Imágenes Procesadas¶

Propósito: Inspección visual del preprocesamiento

  • Muestra comparación de formatos: BGR, RGB y Escala de Grises
  • Valida la calidad de la conversión de formatos
  • Permite identificar problemas en la carga de imágenes

Operaciones Morfológicas: Ninguna (visualización)


CELDA 5: Dilatación Inicial (Batch)¶

Propósito: Primera operación morfológica sobre todas las placas

  • Convierte todas las imágenes a escala de grises
  • Aplica binarización con umbral 127
  • Dilatación: Kernel 3×3, 1 iteración sobre imágenes grises y binarias
  • Almacena resultados en listas para procesamiento posterior

Operaciones Morfológicas:

  • ✅ Dilatación (expanding de regiones blancas)

CELDA 6: Visualización Comparativa de Placas¶

Propósito: Comparación de efectos morfológicos iniciales

  • Muestra 5 versiones de cada placa: Original (color), Grises, Dilatación Grises, Binaria, Dilatación Binaria
  • Permite evaluar el impacto de la dilatación inicial
  • Grid visual con todas las placas procesadas

Operaciones Morfológicas: Ninguna (visualización)


CELDA 7: Análisis con Histogramas¶

Propósito: Análisis cuantitativo del procesamiento

  • Visualiza diferencias antes/después de dilatación
  • Genera histogramas comparativos de distribución de píxeles
  • Calcula diferencias binarias para visualizar píxeles añadidos

Operaciones Morfológicas: Ninguna (análisis)


CELDA 8: Gradiente Morfológico + Umbralización¶

Propósito: Detección de bordes mediante morfología

  • Filtro bilateral para suavizado preservando bordes
  • Gradiente Morfológico: Dilatación - Erosión (kernel 3×3)
  • Umbralización de Otsu sobre el gradiente
  • Cierre morfológico: Kernel 30×5, 3 iteraciones (conectar caracteres)
  • Filtrado de contornos por área mínima

Operaciones Morfológicas:

  • ✅ Dilatación (parte del gradiente)
  • ✅ Erosión (parte del gradiente)
  • ✅ Gradiente Morfológico (Dilation - Erosion)
  • ✅ Cierre (Closing) (unir regiones fragmentadas)

CELDA 9: Detección Inteligente de Región de Placa¶

Propósito: Pre-localización de la zona de la placa

  • Ecualización adaptativa CLAHE
  • Umbralización de Otsu para detectar regiones claras (placas típicamente son claras)
  • Cierre morfológico: Kernel 15×5 (conectar caracteres)
  • Apertura morfológica: Kernel 3×3 (eliminar ruido)
  • Filtrado por aspect ratio (1.5-6.0) y área (>5000)
  • Detección de Canny solo en región de interés

Operaciones Morfológicas:

  • ✅ Cierre (Closing) (conectar caracteres de la placa)
  • ✅ Apertura (Opening) (eliminar ruido pequeño)

CELDA 10: Pipeline Morfológico Completo¶

Propósito: Implementación del pipeline definitivo de detección

  • Filtro bilateral (preservar bordes)
  • Apertura: Kernel 3×3 (limpieza inicial de ruido)
  • Top-Hat: Kernel 35×18 (extraer placas claras del fondo oscuro)
  • Gradiente Morfológico: Kernel 3×3 (resaltar bordes)
  • Combinación ponderada: 70% Top-Hat + 30% Gradiente
  • Binarización de Otsu
  • Cierre agresivo: Kernel 30×5, 3 iteraciones (unir caracteres)
  • Apertura: Kernel 5×5 (limpieza final)
  • Dilatación: Kernel 20×8, 2 iteraciones (expandir región final)
  • Filtrado de contornos por área >2000

Operaciones Morfológicas:

  • ✅ Apertura (Opening) (limpieza de ruido)
  • ✅ Top-Hat (extraer objetos brillantes)
  • ✅ Gradiente Morfológico (detección de bordes)
  • ✅ Cierre (Closing) (conectar caracteres)
  • ✅ Apertura (Opening) (limpieza final)
  • ✅ Dilatación (expansión de región)
  • ✅ Erosión (implícita en operaciones compuestas)

CELDA 11: Filtrado por Aspect Ratio¶

Propósito: Selección del mejor candidato por características geométricas

  • Búsqueda de contornos en máscaras morfológicas finales
  • Filtrado por:
    • Área mínima: 18,000 px
    • Aspect Ratio: 1.9 - 4.0 (placas horizontales)
    • Solidez mínima: 0.25
  • Sistema de scoring para seleccionar mejor candidato
  • Visualización con bounding boxes

Operaciones Morfológicas: Ninguna (post-procesamiento geométrico)


CELDA 12: Recorte de Placas Detectadas¶

Propósito: Extracción de ROI (Región de Interés)

  • Recorte de placas usando bounding boxes de mejor candidato
  • Margen de 10 píxeles para capturar contexto
  • Generación de versiones en color y escala de grises
  • Almacenamiento para fase de enderezado

Operaciones Morfológicas: Ninguna (extracción de ROI)


CELDA 13: Detección y Extracción (Versión Mejorada)¶

Propósito: Pipeline alternativo usando máscaras de Celda 10

  • Utiliza directamente todas_dilataciones_binarias de Celda 10
  • Parámetros adaptativos: AR 1.5-8.0, Área >15,000
  • Scoring basado en área, aspect ratio, solidez y extent
  • Visualización de 3 imágenes: Original, Máscara Final, Placa Extraída
  • Compatible con Celda 14 (Rotación)

Operaciones Morfológicas: Ninguna (usa resultados de Celda 10)


CELDA 14: Corrección de Rotación (Batch)¶

Propósito: Normalización de orientación de placas

  • Estrategia simple: detectar si width < height
  • Rotación automática de 90° para placas verticales
  • Preservación de placas ya horizontales
  • Visualización comparativa antes/después
  • Preparación para OCR o análisis posterior

Operaciones Morfológicas: Ninguna (transformación geométrica)


Resumen de Operaciones Morfológicas Utilizadas¶

Operación Celdas Aplicación en el Pipeline
Dilatación 5, 8, 10 Expansión de regiones, parte del gradiente, expansión final
Erosión 8, 10 Parte del gradiente, refinamiento
Apertura (Opening) 9, 10 Eliminación de ruido pequeño
Cierre (Closing) 8, 9, 10 Conexión de caracteres fragmentados
Gradiente Morfológico 8, 10 Detección de bordes y transiciones
Top-Hat 10 Extracción de placas claras del fondo oscuro

Justificación Técnica¶

Este proyecto demuestra cómo las operaciones morfológicas no son técnicas aisladas, sino herramientas que se combinan estratégicamente. El pipeline utiliza:

  1. Dilatación inicial (Celda 5): Exploración de conectividad
  2. Gradiente (Celda 8): Primera detección de bordes
  3. Opening/Closing (Celda 9): Limpieza y conexión inteligente
  4. Top-Hat (Celda 10): Extracción específica de objetos brillantes
  5. Pipeline completo (Celda 10): Combinación estratégica de todas las operaciones

Cada operación tiene un propósito específico que contribuye al objetivo final: detección robusta de placas vehiculares en condiciones variables de iluminación y calidad.


Nota: Este apéndice complementa la Actividad 4.2 y demuestra la aplicación secuencial de todas las operaciones morfológicas estudiadas en un caso de uso real de producción.

In [1]:
# CELDA 1:
# ===============================================================
# INSTALACIÓN DE LIBRERÍAS
# ===============================================================
!pip install -q pillow-heif opencv-python-headless
!pip install -q rawpy gdown

# ===============================================================
# MANEJO DE DATOS Y COMPUTACIÓN CIENTÍFICA
# ===============================================================
import numpy as np                          # Arrays y operaciones matemáticas

# ===============================================================
# PROCESAMIENTO DE IMÁGENES
# ===============================================================
import cv2                                  # OpenCV para manipulación y filtros
from PIL import Image                       # Manipulación de imágenes
import pillow_heif                          # Formato HEIC
import rawpy                                # Archivos RAW

# ===============================================================
# VISUALIZACIÓN DE DATOS
# ===============================================================
import matplotlib.pyplot as plt             # Gráficos y visualizaciones

# ===============================================================
# UTILIDADES Y MANEJO DEL SISTEMA
# ===============================================================
import os                                   # Operaciones del sistema
from pathlib import Path                    # Manejo de rutas
import shutil                               # Operaciones con archivos
from csv import DictWriter                  # Escritura de CSV
from urllib.request import urlopen          # Solicitudes HTTP
from datetime import datetime               # Manejo de fechas
from zoneinfo import ZoneInfo               # Zonas horarias
import time                                 # Tiempo de ejecución

# ===============================================================
# GOOGLE DRIVE
# ===============================================================
try:
    import gdown                            # Descarga desde Google Drive
except ImportError as e:
    raise SystemExit(
        "No se encontró 'gdown'. Instálalo con:\n\n    pip install gdown\n"
    )

# ===============================================================
# MENSAJE DE CONFIRMACIÓN
# ===============================================================
print("Librerías cargadas y listas para la Visión")
print(f"Timestamp de ejecución: {datetime.now(ZoneInfo('America/Mexico_City')).strftime('%Y-%m-%d %H:%M:%S')}")

try:
    with urlopen('https://ipapi.co/country_name/') as response:
        country = response.read().decode('utf-8').strip()
    print(f"País de ejecución: {country}")
    with urlopen('https://ipapi.co/region/') as response:
        region = response.read().decode('utf-8').strip()
    print(f"Estado/Región de ejecución: {region}")
except Exception as e:
    print("No se pudo determinar el país o estado de ejecución (posiblemente sin conexión a internet).")

notebook_start_time = time.time()
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 5.5/5.5 MB 44.8 MB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.9/1.9 MB 20.0 MB/s eta 0:00:00
Librerías cargadas y listas para la Visión
Timestamp de ejecución: 2025-10-05 10:01:27
País de ejecución: United States
Estado/Región de ejecución: South Carolina
In [2]:
# CELDA 2:
# ===============================================================
# 1) INVENTARIO FUENTE: (nombre_original, drive_id)
# ===============================================================
fuente = [
    # De tu lista 00–11
    ("Plate 00.JPG",  "17J6nKFJjKZWWXVqeoNonfBZ4gkF0N5O4"),
    ("Plate 01.HEIC", "1o_QIWc1TW55r26u_PWRFoeysvIIlGFMH"),
    ("Plate 02.JPG",  "13CTNshx-qQuzacpDBJoYSI9cxMD58dZD"),
    ("Plate 03.HEIC", "1U_nWjWBJHG44rdaQ2cOGRvYZTaxdMmSx"),
    ("Plate 04.HEIC", "16kRnLY_uNZrc4JCK2D68hZ7WYE6QF9p9"),
    ("Plate 05.HEIC", "1jXp0VhxKIczCkdySnFxBMr2Cm9ekI14b"),
    ("Plate 06.HEIC", "1ib4v9YAE85LWhQ0CXGHbApc7fvCWccBL"),
    ("Plate 07.HEIC", "1b7SvhrbfCEsd5Iu393oe81hFI8NpNilN"),
    ("Plate 08.HEIC", "1NzoSVcbmPk_eH54GRmWneETb12EhtoCF"),
    ("Plate 09.HEIC", "1Ku7m5t6BDixMvCxbT5WA_1-5zgYM0hK0"),
    ("Plate 10.heic", "19W2zCiE5syb0kzzmk3kPIy7ibDjbt_m_"),
    ("Plate 11.HEIC", "1p_xtpHrKZ_c7N0xH-fa5bn01Bf4z9817"),
    ("p2b. Rebull Plate.DNG",        "1QsnlZcK-2YE35o3AMiX81xH-LgITWGsh"),
    ("pb2. MCL878 Florida Plate.DNG","12QMq8QqzpAQCJxNrqObcjQGiSOiugBnq"),
    ("pb2. Maine Plate.DNG",         "1hHNc_sb93jDYtUHct5YakVq8Rcf_U3uA"),
    ("pb2. Veteran Plate.DNG",       "19jGR_kfcyCS824HUYtdE2rdTvSiDHr4G"),
    ("p2b. MA Night 01.HEIC",        "1jHCFZs7qV67WbHXyVQt_YBr_-jcoKmqi"),
    ("p2b. MA Night 02.HEIC",        "1hnh6hPdQ-vdmAiWBGF-bmrEQ6Jo6Cgfb"),
    ("p2b. MA Night 03.HEIC",        "1uaTxrYpgUiSUsJ7SuL85ytffJ767uPGL"),
]

# ===============================================================
# 2) FUNCIÓN: generar nombres "Plate XX" secuenciales (conserva extensión)
# ===============================================================
def generar_mapeo_secuencial(items, prefijo="Plate "):
    """
    items: lista de tuplas (nombre_original, drive_id)
    return:
      - imagenes_plates: dict {nombre_nuevo: drive_id}
      - renombres: dict {nombre_original: nombre_nuevo}
      - filas_csv: lista de dicts para exportar mapping
    """
    imagenes_plates = {}
    renombres = {}
    filas_csv = []

    for i, (nombre, file_id) in enumerate(items):
        ext = Path(nombre).suffix
        ext = ext.upper() if ext else ""
        nombre_nuevo = f"{prefijo}{i:02d}{ext}"

        imagenes_plates[nombre_nuevo] = file_id
        renombres[nombre] = nombre_nuevo
        filas_csv.append({
            "old_name": nombre,
            "new_name": nombre_nuevo,
            "drive_id": file_id,
        })

    return imagenes_plates, renombres, filas_csv

# ===============================================================
# 3) CONSTRUIR LOS DICCIONARIOS (secuenciales)
# ===============================================================
imagenes_plates, renombres, filas_csv = generar_mapeo_secuencial(fuente)

# ===============================================================
# 4) LIMPIEZA Y PREPARACIÓN
# ===============================================================
print("\n🧹 Limpiando directorio...")
directorio = "p2b_plates"

if os.path.exists(directorio):
    shutil.rmtree(directorio)
    print(f"✓ Carpeta '{directorio}' eliminada")

os.makedirs(directorio, exist_ok=True)
print(f"✓ Carpeta '{directorio}' creada\n")

# Guardamos el mapping como CSV para referencia
mapping_csv = os.path.join(directorio, "mapping.csv")
with open(mapping_csv, "w", newline="", encoding="utf-8") as f:
    writer = DictWriter(f, fieldnames=["old_name", "new_name", "drive_id"])
    writer.writeheader()
    writer.writerows(filas_csv)
print(f"🗂️  Mapping guardado en: {mapping_csv}\n")

# ===============================================================
# 5) DESCARGA DE IMÁGENES (usa nombres NUEVOS secuenciales)
# ===============================================================
print("📥 Descargando imágenes de placas...\n")
for nombre_nuevo, file_id in imagenes_plates.items():
    output_path = os.path.join(directorio, nombre_nuevo)
    print(f"⬇️  Descargando '{nombre_nuevo}' (id={file_id})...")
    # gdown permite usar id=... directamente
    gdown.download(id=file_id, output=output_path, quiet=False)

print("\n✅ Descarga completada")
print("Resumen:")
print(f" - Total de elementos: {len(imagenes_plates)}")
print(f" - Directorio de salida: {os.path.abspath(directorio)}")
🧹 Limpiando directorio...
✓ Carpeta 'p2b_plates' creada

🗂️  Mapping guardado en: p2b_plates/mapping.csv

📥 Descargando imágenes de placas...

⬇️  Descargando 'Plate 00.JPG' (id=17J6nKFJjKZWWXVqeoNonfBZ4gkF0N5O4)...
Downloading...
From: https://drive.google.com/uc?id=17J6nKFJjKZWWXVqeoNonfBZ4gkF0N5O4
To: /content/p2b_plates/Plate 00.JPG
100%|██████████| 1.19M/1.19M [00:00<00:00, 90.7MB/s]
⬇️  Descargando 'Plate 01.HEIC' (id=1o_QIWc1TW55r26u_PWRFoeysvIIlGFMH)...
Downloading...
From: https://drive.google.com/uc?id=1o_QIWc1TW55r26u_PWRFoeysvIIlGFMH
To: /content/p2b_plates/Plate 01.HEIC
100%|██████████| 2.93M/2.93M [00:00<00:00, 87.7MB/s]
⬇️  Descargando 'Plate 02.JPG' (id=13CTNshx-qQuzacpDBJoYSI9cxMD58dZD)...
Downloading...
From: https://drive.google.com/uc?id=13CTNshx-qQuzacpDBJoYSI9cxMD58dZD
To: /content/p2b_plates/Plate 02.JPG
100%|██████████| 450k/450k [00:00<00:00, 36.6MB/s]
⬇️  Descargando 'Plate 03.HEIC' (id=1U_nWjWBJHG44rdaQ2cOGRvYZTaxdMmSx)...
Downloading...
From: https://drive.google.com/uc?id=1U_nWjWBJHG44rdaQ2cOGRvYZTaxdMmSx
To: /content/p2b_plates/Plate 03.HEIC
100%|██████████| 2.23M/2.23M [00:00<00:00, 34.5MB/s]
⬇️  Descargando 'Plate 04.HEIC' (id=16kRnLY_uNZrc4JCK2D68hZ7WYE6QF9p9)...
Downloading...
From: https://drive.google.com/uc?id=16kRnLY_uNZrc4JCK2D68hZ7WYE6QF9p9
To: /content/p2b_plates/Plate 04.HEIC
100%|██████████| 2.23M/2.23M [00:00<00:00, 23.1MB/s]
⬇️  Descargando 'Plate 05.HEIC' (id=1jXp0VhxKIczCkdySnFxBMr2Cm9ekI14b)...
Downloading...
From: https://drive.google.com/uc?id=1jXp0VhxKIczCkdySnFxBMr2Cm9ekI14b
To: /content/p2b_plates/Plate 05.HEIC
100%|██████████| 3.96M/3.96M [00:00<00:00, 200MB/s]
⬇️  Descargando 'Plate 06.HEIC' (id=1ib4v9YAE85LWhQ0CXGHbApc7fvCWccBL)...
Downloading...
From: https://drive.google.com/uc?id=1ib4v9YAE85LWhQ0CXGHbApc7fvCWccBL
To: /content/p2b_plates/Plate 06.HEIC
100%|██████████| 3.96M/3.96M [00:00<00:00, 137MB/s]
⬇️  Descargando 'Plate 07.HEIC' (id=1b7SvhrbfCEsd5Iu393oe81hFI8NpNilN)...
Downloading...
From: https://drive.google.com/uc?id=1b7SvhrbfCEsd5Iu393oe81hFI8NpNilN
To: /content/p2b_plates/Plate 07.HEIC
100%|██████████| 2.16M/2.16M [00:00<00:00, 127MB/s]
⬇️  Descargando 'Plate 08.HEIC' (id=1NzoSVcbmPk_eH54GRmWneETb12EhtoCF)...
Downloading...
From: https://drive.google.com/uc?id=1NzoSVcbmPk_eH54GRmWneETb12EhtoCF
To: /content/p2b_plates/Plate 08.HEIC
100%|██████████| 1.86M/1.86M [00:00<00:00, 157MB/s]
⬇️  Descargando 'Plate 09.HEIC' (id=1Ku7m5t6BDixMvCxbT5WA_1-5zgYM0hK0)...
Downloading...
From: https://drive.google.com/uc?id=1Ku7m5t6BDixMvCxbT5WA_1-5zgYM0hK0
To: /content/p2b_plates/Plate 09.HEIC
100%|██████████| 1.86M/1.86M [00:00<00:00, 76.9MB/s]
⬇️  Descargando 'Plate 10.HEIC' (id=19W2zCiE5syb0kzzmk3kPIy7ibDjbt_m_)...
Downloading...
From: https://drive.google.com/uc?id=19W2zCiE5syb0kzzmk3kPIy7ibDjbt_m_
To: /content/p2b_plates/Plate 10.HEIC
100%|██████████| 2.27M/2.27M [00:00<00:00, 129MB/s]
⬇️  Descargando 'Plate 11.HEIC' (id=1p_xtpHrKZ_c7N0xH-fa5bn01Bf4z9817)...
Downloading...
From: https://drive.google.com/uc?id=1p_xtpHrKZ_c7N0xH-fa5bn01Bf4z9817
To: /content/p2b_plates/Plate 11.HEIC
100%|██████████| 2.64M/2.64M [00:00<00:00, 177MB/s]
⬇️  Descargando 'Plate 12.DNG' (id=1QsnlZcK-2YE35o3AMiX81xH-LgITWGsh)...
Downloading...
From: https://drive.google.com/uc?id=1QsnlZcK-2YE35o3AMiX81xH-LgITWGsh
To: /content/p2b_plates/Plate 12.DNG
100%|██████████| 82.6M/82.6M [00:00<00:00, 137MB/s]
⬇️  Descargando 'Plate 13.DNG' (id=12QMq8QqzpAQCJxNrqObcjQGiSOiugBnq)...
Downloading...
From: https://drive.google.com/uc?id=12QMq8QqzpAQCJxNrqObcjQGiSOiugBnq
To: /content/p2b_plates/Plate 13.DNG
100%|██████████| 69.3M/69.3M [00:00<00:00, 146MB/s]
⬇️  Descargando 'Plate 14.DNG' (id=1hHNc_sb93jDYtUHct5YakVq8Rcf_U3uA)...
Downloading...
From: https://drive.google.com/uc?id=1hHNc_sb93jDYtUHct5YakVq8Rcf_U3uA
To: /content/p2b_plates/Plate 14.DNG
100%|██████████| 66.5M/66.5M [00:00<00:00, 118MB/s]
⬇️  Descargando 'Plate 15.DNG' (id=19jGR_kfcyCS824HUYtdE2rdTvSiDHr4G)...
Downloading...
From: https://drive.google.com/uc?id=19jGR_kfcyCS824HUYtdE2rdTvSiDHr4G
To: /content/p2b_plates/Plate 15.DNG
100%|██████████| 86.7M/86.7M [00:00<00:00, 152MB/s]
⬇️  Descargando 'Plate 16.HEIC' (id=1jHCFZs7qV67WbHXyVQt_YBr_-jcoKmqi)...
Downloading...
From: https://drive.google.com/uc?id=1jHCFZs7qV67WbHXyVQt_YBr_-jcoKmqi
To: /content/p2b_plates/Plate 16.HEIC
100%|██████████| 3.05M/3.05M [00:00<00:00, 93.5MB/s]
⬇️  Descargando 'Plate 17.HEIC' (id=1hnh6hPdQ-vdmAiWBGF-bmrEQ6Jo6Cgfb)...
Downloading...
From: https://drive.google.com/uc?id=1hnh6hPdQ-vdmAiWBGF-bmrEQ6Jo6Cgfb
To: /content/p2b_plates/Plate 17.HEIC
100%|██████████| 3.22M/3.22M [00:00<00:00, 149MB/s]
⬇️  Descargando 'Plate 18.HEIC' (id=1uaTxrYpgUiSUsJ7SuL85ytffJ767uPGL)...
Downloading...
From: https://drive.google.com/uc?id=1uaTxrYpgUiSUsJ7SuL85ytffJ767uPGL
To: /content/p2b_plates/Plate 18.HEIC
100%|██████████| 2.76M/2.76M [00:00<00:00, 55.8MB/s]
✅ Descarga completada
Resumen:
 - Total de elementos: 19
 - Directorio de salida: /content/p2b_plates

In [3]:
# CELDA 3:
# ===================================================================
# FUNCIÓN AUXILIAR PARA REDIMENSIONAR IMÁGENES
# ===================================================================
def resize_image(image, max_width=800):
    """
    Redimensiona una imagen manteniendo su relación de aspecto.
    """
    height, width = image.shape[:2]
    if width > max_width:
        ratio = max_width / width
        new_dimensions = (max_width, int(height * ratio))
        return cv2.resize(image, new_dimensions, interpolation=cv2.INTER_AREA)
    return image

# ===================================================================
# PROCESAMIENTO Y REDIMENSION DE IMÁGENES (p2b_plates)
# ===================================================================
# --- Listas NUEVAS solo para las imágenes de matrículas (plates) ---
imagenes_originales_p2b = []
imagenes_rgb_p2b = []
imagenes_grises_p2b = []
imagenes_binarias_p2b = []
imagenes_binarias_invertidas_p2b = []
nombres_archivos_procesados_p2b = []

# --- Directorio a procesar ---
directorio = "p2b_plates"

print(f"\n📂 Procesando todas las imágenes del directorio: '{directorio}'...")

try:
    archivos = [f for f in os.listdir(directorio) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.dng', '.heic', '.heif'))]

    for nombre_archivo in archivos:
        ruta_completa = os.path.join(directorio, nombre_archivo)
        img = None

        if nombre_archivo.lower().endswith('.dng'):
            print(f" - Leyendo archivo RAW: {nombre_archivo}")
            with rawpy.imread(ruta_completa) as raw:
                rgb_array = raw.postprocess()
                img = cv2.cvtColor(rgb_array, cv2.COLOR_RGB2BGR)

        elif nombre_archivo.lower().endswith(('.heic', '.heif')):
            print(f" - Leyendo archivo HEIC: {nombre_archivo}")
            heif_file = pillow_heif.read_heif(ruta_completa)
            image_pil = Image.frombytes(
                heif_file.mode, heif_file.size, heif_file.data, "raw",
            )
            img = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)

        else:
            print(f" - Leyendo archivo: {nombre_archivo}")
            img = cv2.imread(ruta_completa)

        if img is not None:
            img = resize_image(img, max_width=800)
            rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            binr = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
            invert = cv2.bitwise_not(binr)

            # Guardar en las listas de matrículas (plates)
            imagenes_originales_p2b.append(img)
            imagenes_rgb_p2b.append(rgb)
            imagenes_grises_p2b.append(gray)
            imagenes_binarias_p2b.append(binr)
            imagenes_binarias_invertidas_p2b.append(invert)
            nombres_archivos_procesados_p2b.append(nombre_archivo)
        else:
            print(f"⚠️ No se pudo leer el archivo: {nombre_archivo}")

except FileNotFoundError:
    print(f"❌ Error: El directorio '{directorio}' no fue encontrado.")
except Exception as e:
    print(f"❌ Ocurrió un error inesperado al procesar el directorio '{directorio}': {e}")

# --- Mensaje final ---
print("\n✅ ¡Procesamiento completado!")
print(f" - Se cargaron {len(imagenes_originales_p2b)} imágenes en las listas de matrículas (plates).")
📂 Procesando todas las imágenes del directorio: 'p2b_plates'...
 - Leyendo archivo HEIC: Plate 01.HEIC
 - Leyendo archivo HEIC: Plate 17.HEIC
 - Leyendo archivo: Plate 00.JPG
 - Leyendo archivo HEIC: Plate 07.HEIC
 - Leyendo archivo: Plate 02.JPG
 - Leyendo archivo HEIC: Plate 18.HEIC
 - Leyendo archivo HEIC: Plate 04.HEIC
 - Leyendo archivo HEIC: Plate 06.HEIC
 - Leyendo archivo HEIC: Plate 10.HEIC
 - Leyendo archivo RAW: Plate 13.DNG
 - Leyendo archivo RAW: Plate 12.DNG
 - Leyendo archivo HEIC: Plate 11.HEIC
 - Leyendo archivo HEIC: Plate 09.HEIC
 - Leyendo archivo HEIC: Plate 16.HEIC
 - Leyendo archivo HEIC: Plate 03.HEIC
 - Leyendo archivo RAW: Plate 14.DNG
 - Leyendo archivo HEIC: Plate 08.HEIC
 - Leyendo archivo HEIC: Plate 05.HEIC
 - Leyendo archivo RAW: Plate 15.DNG

✅ ¡Procesamiento completado!
 - Se cargaron 19 imágenes en las listas de matrículas (plates).
In [4]:
# CELDA 4:
# --- Función adaptada para visualizar cualquier set de imágenes ---
def mostrar_comparacion_p2(img_bgr, img_rgb, img_gris, titulo_principal=""):
    """
    Muestra tres versiones de una imagen: BGR, RGB y Escala de Grises.
    """
    fig = plt.figure(figsize=(15, 6))
    fig.suptitle(titulo_principal, fontsize=16)

    # Subplot 1: Imagen Original (BGR)
    fig.add_subplot(1, 3, 1)
    plt.title('Formato BGR (Original)')
    plt.imshow(img_bgr)
    plt.axis('off')

    # Subplot 2: Imagen en formato RGB
    fig.add_subplot(1, 3, 2)
    plt.title('Formato RGB (Corregido)')
    plt.imshow(img_rgb)
    plt.axis('off')

    # Subplot 3: Imagen en Escala de Grises
    fig.add_subplot(1, 3, 3)
    plt.title('Escala de Grises')
    plt.imshow(img_gris, cmap="gray")
    plt.axis('off')

    plt.show()

# --- Bucle para encontrar y desplegar las imágenes de las placas ---
print("🖼️ Mostrando las imágenes de las placas (Plates)...")

# Iteramos a través de la lista de nombres de archivo de p2b junto con su índice
for i, nombre_archivo in enumerate(nombres_archivos_procesados_p2b):
    # Llamamos a la función de visualización para cada imagen
    # Usamos el índice 'i' para obtener las imágenes correctas de cada lista p2b
    mostrar_comparacion_p2(
        img_bgr=imagenes_originales_p2b[i],
        img_rgb=imagenes_rgb_p2b[i],
        img_gris=imagenes_grises_p2b[i],
        titulo_principal=nombres_archivos_procesados_p2b[i]
    )
🖼️ Mostrando las imágenes de las placas (Plates)...
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
In [5]:
# CELDA 5:
# ===================================================================
# PROCESAMIENTO Y DILATACIÓN DE MÚLTIPLES IMÁGENES
# ===================================================================

# Listas para almacenar los resultados de todas las placas
todas_matriculas_gris = []
todas_matriculas_binarias = []
todas_dilataciones_gris = []
todas_dilataciones_binarias = []

print(f"🔄 Procesando {len(imagenes_originales_p2b)} placas...")
print("="*60)

# --- Procesar cada imagen de la lista ---
for idx, imagen_original_color in enumerate(imagenes_originales_p2b):
    print(f"Procesando placa {idx + 1}/{len(imagenes_originales_p2b)}...", end=" ")

    # --- 1. Convertir a escala de grises ---
    matricula_gris = cv2.cvtColor(imagen_original_color, cv2.COLOR_BGR2GRAY)

    # --- 2. Binarización de la imagen ---
    # Píxeles más oscuros que 127 serán negros, los más claros serán blancos
    valor_umbral = 127
    _, matricula_binaria = cv2.threshold(matricula_gris, valor_umbral, 255, cv2.THRESH_BINARY)

    # --- 3. Aplicación de Dilatación ---
    # Kernel de 3x3 para dilatación suave
    kernel = np.ones((3, 3), np.uint8)

    # Aplicar dilatación a imagen en gris y binaria
    dilatacion_gris = cv2.dilate(matricula_gris, kernel, iterations=1)
    dilatacion_binaria = cv2.dilate(matricula_binaria, kernel, iterations=1)

    # --- 4. Guardar resultados en las listas ---
    todas_matriculas_gris.append(matricula_gris)
    todas_matriculas_binarias.append(matricula_binaria)
    todas_dilataciones_gris.append(dilatacion_gris)
    todas_dilataciones_binarias.append(dilatacion_binaria)

    print("✅")

print("="*60)
print(f"✅ Procesamiento completado para {len(imagenes_originales_p2b)} placas.")
print("📊 Las imágenes están almacenadas en:")
print("   - todas_matriculas_gris")
print("   - todas_matriculas_binarias")
print("   - todas_dilataciones_gris")
print("   - todas_dilataciones_binarias")
🔄 Procesando 19 placas...
============================================================
Procesando placa 1/19... ✅
Procesando placa 2/19... ✅
Procesando placa 3/19... ✅
Procesando placa 4/19... ✅
Procesando placa 5/19... ✅
Procesando placa 6/19... ✅
Procesando placa 7/19... ✅
Procesando placa 8/19... ✅
Procesando placa 9/19... ✅
Procesando placa 10/19... ✅
Procesando placa 11/19... ✅
Procesando placa 12/19... ✅
Procesando placa 13/19... ✅
Procesando placa 14/19... ✅
Procesando placa 15/19... ✅
Procesando placa 16/19... ✅
Procesando placa 17/19... ✅
Procesando placa 18/19... ✅
Procesando placa 19/19... ✅
============================================================
✅ Procesamiento completado para 19 placas.
📊 Las imágenes están almacenadas en:
   - todas_matriculas_gris
   - todas_matriculas_binarias
   - todas_dilataciones_gris
   - todas_dilataciones_binarias
In [6]:
# CELDA 6:
# ===================================================================
# VISUALIZACIÓN DE TODAS LAS PLACAS
# ===================================================================

num_placas = len(imagenes_originales_p2b)
filas = num_placas
columnas = 4  # Original, Gris, Binaria, Dilatación Binaria

plt.figure(figsize=(16, 4 * num_placas))

for idx in range(num_placas):
    # Original
    plt.subplot(filas, columnas, idx * columnas + 1)
    plt.imshow(cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2RGB))
    plt.title(f'Placa {idx + 1} - Original')
    plt.axis('off')

    # Escala de grises
    plt.subplot(filas, columnas, idx * columnas + 2)
    plt.imshow(todas_matriculas_gris[idx], cmap='gray')
    plt.title('Escala de Grises')
    plt.axis('off')

    # Binarizada
    plt.subplot(filas, columnas, idx * columnas + 3)
    plt.imshow(todas_matriculas_binarias[idx], cmap='gray')
    plt.title('Binarizada')
    plt.axis('off')

    # Dilatación binaria
    plt.subplot(filas, columnas, idx * columnas + 4)
    plt.imshow(todas_dilataciones_binarias[idx], cmap='gray')
    plt.title('Dilatación')
    plt.axis('off')

plt.tight_layout()
plt.show()

print(f"✅ Visualizadas {num_placas} placas procesadas")
No description has been provided for this image
✅ Visualizadas 19 placas procesadas
In [7]:
# CELDA 7:
# ===================================================================
# VISUALIZACIÓN COMPARATIVA DE TODAS LAS PLACAS
# ===================================================================

num_placas = len(imagenes_originales_p2b)

print(f"📊 Mostrando comparativa de {num_placas} placas procesadas")
print("="*60)

# Crear una figura con subplots para cada placa
# Cada placa tendrá 5 imágenes (1 fila x 5 columnas), así que necesitamos num_placas filas
fig, axes = plt.subplots(num_placas, 5, figsize=(20, 4 * num_placas))

# Si solo hay una placa, aseguramos que axes sea 2D
if num_placas == 1:
    axes = axes.reshape(1, -1)

for idx in range(num_placas):
    # --- Columna 1: Original en Color ---
    axes[idx, 0].imshow(cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2RGB))
    axes[idx, 0].set_title(f'Placa {idx + 1} - Original (Color)', fontweight='bold')
    axes[idx, 0].axis('off')

    # --- Columna 2: Original en Grises ---
    axes[idx, 1].imshow(todas_matriculas_gris[idx], cmap='gray')
    axes[idx, 1].set_title('Grises (Original)')
    axes[idx, 1].axis('off')

    # --- Columna 3: Dilatación en Grises ---
    axes[idx, 2].imshow(todas_dilataciones_gris[idx], cmap='gray')
    axes[idx, 2].set_title('Grises con Dilatación')
    axes[idx, 2].axis('off')

    # --- Columna 4: Imagen Binaria ---
    axes[idx, 3].imshow(todas_matriculas_binarias[idx], cmap='gray')
    axes[idx, 3].set_title('Imagen Binaria')
    axes[idx, 3].axis('off')

    # --- Columna 5: Dilatación Binaria ---
    axes[idx, 4].imshow(todas_dilataciones_binarias[idx], cmap='gray')
    axes[idx, 4].set_title('Binaria con Dilatación')
    axes[idx, 4].axis('off')

plt.tight_layout()
plt.show()

print(f"✅ Visualización completada para {num_placas} placas")
📊 Mostrando comparativa de 19 placas procesadas
============================================================
No description has been provided for this image
✅ Visualización completada para 19 placas
In [8]:
# CELDA 8:
# ===================================================================
# VISUALIZACIÓN EN LÍNEA E HISTOGRAMA COMBINADO (BATCH)
# ===================================================================

num_placas = len(imagenes_originales_p2b)

print(f"📊 Mostrando análisis detallado de {num_placas} placas")
print("="*60)

for idx in range(num_placas):
    print(f"\n🔍 Analizando Placa {idx + 1}/{num_placas}")

    # --- 1. Mostramos las tres imágenes principales en una sola fila ---
    plt.figure(figsize=(18, 5))

    # Gráfico 1: Imagen Original en escala de grises
    plt.subplot(1, 3, 1)
    plt.imshow(todas_matriculas_gris[idx], cmap='gray')
    plt.title(f'Placa {idx + 1} - Original (Grises)', fontweight='bold')
    plt.axis('off')

    # Gráfico 2: Imagen con Dilatación
    plt.subplot(1, 3, 2)
    plt.imshow(todas_dilataciones_gris[idx], cmap='gray')
    plt.title('Imagen con Dilatación', fontweight='bold')
    plt.axis('off')

    # Gráfico 3: Diferencia de Píxeles
    diferencia_binaria = cv2.absdiff(todas_matriculas_binarias[idx],
                                     todas_dilataciones_binarias[idx])
    plt.subplot(1, 3, 3)
    plt.imshow(diferencia_binaria, cmap='gray')
    plt.title('Diferencia (Píxeles añadidos)', fontweight='bold')
    plt.axis('off')

    plt.tight_layout()
    plt.show()

    # --- 2. Comparación de Histogramas (en un solo gráfico, abajo) ---
    plt.figure(figsize=(10, 6))

    # Dibujamos ambos histogramas juntos
    plt.hist(todas_matriculas_gris[idx].ravel(), 256, range=[0, 256],
             label='Original', color='blue', alpha=0.7)
    plt.hist(todas_dilataciones_gris[idx].ravel(), 256, range=[0, 256],
             label='Con Dilatación', color='red', alpha=0.7)

    plt.title(f'Placa {idx + 1} - Comparación de Histogramas',
              fontsize=12, fontweight='bold')
    plt.xlabel('Intensidad de Píxel')
    plt.ylabel('Cantidad de Píxeles')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.show()

print("\n" + "="*60)
print(f"✅ Análisis completado para {num_placas} placas")
📊 Mostrando análisis detallado de 19 placas
============================================================

🔍 Analizando Placa 1/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 2/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 3/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 4/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 5/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 6/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 7/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 8/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 9/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 10/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 11/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 12/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 13/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 14/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 15/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 16/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 17/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 18/19
No description has been provided for this image
No description has been provided for this image
🔍 Analizando Placa 19/19
No description has been provided for this image
No description has been provided for this image
============================================================
✅ Análisis completado para 19 placas
In [9]:
# CELDA 9:
# ===================================================================
# GRADIENTE MORFOLÓGICO + UMBRALIZACIÓN
# ===================================================================

import cv2
import numpy as np
import matplotlib.pyplot as plt

num_placas = len(imagenes_originales_p2b)

print("="*70)
print("🎯 DETECCIÓN POR GRADIENTE MORFOLÓGICO")
print("="*70)

todas_imagenes_binarias = []

for idx in range(num_placas):
    print(f"\n{'─'*70}")
    print(f"📋 PLACA {idx + 1}/{num_placas}")
    print(f"{'─'*70}")

    img_original = imagenes_originales_p2b[idx].copy()

    # Convertir a escala de grises
    img_gris = cv2.cvtColor(img_original, cv2.COLOR_BGR2GRAY)

    print("   🔹 Aplicando filtro bilateral para suavizar...")
    img_bilateral = cv2.bilateralFilter(img_gris, 11, 17, 17)

    # === GRADIENTE MORFOLÓGICO (detecta cambios de intensidad) ===
    print("   🔹 Calculando gradiente morfológico...")
    # Kernel pequeño para detectar bordes
    kernel_gradiente = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))

    # Gradiente = Dilatación - Erosión
    img_dilatada_temp = cv2.dilate(img_bilateral, kernel_gradiente, iterations=1)
    img_erosionada_temp = cv2.erode(img_bilateral, kernel_gradiente, iterations=1)
    img_gradiente = cv2.subtract(img_dilatada_temp, img_erosionada_temp)

    # === UMBRALIZACIÓN ADAPTATIVA ===
    print("   🔹 Umbralización adaptativa...")
    # Otsu sobre el gradiente (detecta zonas con mucho cambio)
    _, img_umbral = cv2.threshold(img_gradiente, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

    # === CIERRE MORFOLÓGICO (conectar regiones) ===
    print("   🔹 Cierre morfológico agresivo...")
    kernel_cierre = cv2.getStructuringElement(cv2.MORPH_RECT, (30, 5))
    img_cerrada = cv2.morphologyEx(img_umbral, cv2.MORPH_CLOSE, kernel_cierre, iterations=3)

    # === ELIMINAR RUIDO PEQUEÑO ===
    print("   🔹 Eliminando ruido...")
    # Encontrar contornos
    contornos_temp, _ = cv2.findContours(img_cerrada.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # Crear imagen limpia - solo contornos grandes
    img_limpia = np.zeros_like(img_cerrada)

    for cnt in contornos_temp:
        area = cv2.contourArea(cnt)
        if area > 1000:  # Mantener solo regiones significativas
            cv2.drawContours(img_limpia, [cnt], -1, 255, thickness=cv2.FILLED)

    todas_imagenes_binarias.append(img_limpia)

    # Diagnóstico
    contornos_finales, _ = cv2.findContours(img_limpia.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    print(f"   ✓ Regiones detectadas: {len(contornos_finales)}")

print(f"\n{'='*70}")
print(f"✅ Procesamiento completado")
print(f"{'='*70}\n")

# === VISUALIZACIÓN ===
fig, axes = plt.subplots(num_placas, 5, figsize=(25, 5 * num_placas))

if num_placas == 1:
    axes = axes.reshape(1, -1)

for idx in range(num_placas):
    img_gris = cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2GRAY)
    img_bilateral = cv2.bilateralFilter(img_gris, 11, 17, 17)

    kernel_gradiente = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    img_dilatada_temp = cv2.dilate(img_bilateral, kernel_gradiente, iterations=1)
    img_erosionada_temp = cv2.erode(img_bilateral, kernel_gradiente, iterations=1)
    img_gradiente = cv2.subtract(img_dilatada_temp, img_erosionada_temp)

    _, img_umbral = cv2.threshold(img_gradiente, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

    # Original
    axes[idx, 0].imshow(cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2RGB))
    axes[idx, 0].set_title(f'Placa {idx + 1}', fontsize=10, fontweight='bold')
    axes[idx, 0].axis('off')

    # Bilateral
    axes[idx, 1].imshow(img_bilateral, cmap='gray')
    axes[idx, 1].set_title('Bilateral', fontsize=10)
    axes[idx, 1].axis('off')

    # Gradiente
    axes[idx, 2].imshow(img_gradiente, cmap='gray')
    axes[idx, 2].set_title('Gradiente Morfológico', fontsize=10)
    axes[idx, 2].axis('off')

    # Umbral
    axes[idx, 3].imshow(img_umbral, cmap='gray')
    axes[idx, 3].set_title('Umbralización', fontsize=10)
    axes[idx, 3].axis('off')

    # Final
    axes[idx, 4].imshow(todas_imagenes_binarias[idx], cmap='gray')
    axes[idx, 4].set_title('RESULTADO FINAL', fontsize=10, color='green', fontweight='bold')
    axes[idx, 4].axis('off')

plt.tight_layout()
plt.show()

print("Observa la columna 'Gradiente Morfológico' - debería resaltar las placas")
======================================================================
🎯 DETECCIÓN POR GRADIENTE MORFOLÓGICO
======================================================================

──────────────────────────────────────────────────────────────────────
📋 PLACA 1/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 1

──────────────────────────────────────────────────────────────────────
📋 PLACA 2/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 4

──────────────────────────────────────────────────────────────────────
📋 PLACA 3/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 3

──────────────────────────────────────────────────────────────────────
📋 PLACA 4/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 1

──────────────────────────────────────────────────────────────────────
📋 PLACA 5/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 1

──────────────────────────────────────────────────────────────────────
📋 PLACA 6/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 7

──────────────────────────────────────────────────────────────────────
📋 PLACA 7/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 6

──────────────────────────────────────────────────────────────────────
📋 PLACA 8/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 2

──────────────────────────────────────────────────────────────────────
📋 PLACA 9/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 2

──────────────────────────────────────────────────────────────────────
📋 PLACA 10/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 10

──────────────────────────────────────────────────────────────────────
📋 PLACA 11/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 6

──────────────────────────────────────────────────────────────────────
📋 PLACA 12/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 3

──────────────────────────────────────────────────────────────────────
📋 PLACA 13/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 2

──────────────────────────────────────────────────────────────────────
📋 PLACA 14/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 3

──────────────────────────────────────────────────────────────────────
📋 PLACA 15/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 6

──────────────────────────────────────────────────────────────────────
📋 PLACA 16/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 6

──────────────────────────────────────────────────────────────────────
📋 PLACA 17/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 2

──────────────────────────────────────────────────────────────────────
📋 PLACA 18/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 2

──────────────────────────────────────────────────────────────────────
📋 PLACA 19/19
──────────────────────────────────────────────────────────────────────
   🔹 Aplicando filtro bilateral para suavizar...
   🔹 Calculando gradiente morfológico...
   🔹 Umbralización adaptativa...
   🔹 Cierre morfológico agresivo...
   🔹 Eliminando ruido...
   ✓ Regiones detectadas: 7

======================================================================
✅ Procesamiento completado
======================================================================

No description has been provided for this image
Observa la columna 'Gradiente Morfológico' - debería resaltar las placas
In [10]:
# CELDA 10:
# ===================================================================
# DETECCIÓN INTELIGENTE DE REGIÓN DE PLACA
# ===================================================================

import cv2
import numpy as np
import matplotlib.pyplot as plt

num_placas = len(imagenes_originales_p2b)

print("="*70)
print("🎯 DETECCIÓN INTELIGENTE DE REGIÓN DE PLACA")
print("="*70)

todas_imagenes_binarias = []
todas_regiones_placa = []  # Máscaras de región de placa

for idx in range(num_placas):
    print(f"\n{'─'*70}")
    print(f"📋 PLACA {idx + 1}/{num_placas}")
    print(f"{'─'*70}")

    img_original = imagenes_originales_p2b[idx].copy()

    # === PASO 1: Escala de grises ===
    img_gris = cv2.cvtColor(img_original, cv2.COLOR_BGR2GRAY)

    # === PASO 2: Ecualización adaptativa (mejor que normal) ===
    print("   🔹 Ecualización adaptativa...")
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
    img_clahe = clahe.apply(img_gris)

    # === PASO 3: Umbralización de Otsu para encontrar regiones claras ===
    print("   🔹 Binarización de Otsu (detectar región clara de placa)...")
    # Las placas suelen ser más claras que el resto del auto
    blur = cv2.GaussianBlur(img_clahe, (5, 5), 0)
    _, img_otsu = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

    # === PASO 4: Operaciones morfológicas para limpiar ===
    print("   🔹 Limpieza morfológica...")
    # Cerrar huecos pequeños
    kernel_cierre = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5))
    img_cerrada = cv2.morphologyEx(img_otsu, cv2.MORPH_CLOSE, kernel_cierre)

    # Eliminar ruido pequeño
    kernel_apertura = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    img_limpia = cv2.morphologyEx(img_cerrada, cv2.MORPH_OPEN, kernel_apertura)

    # === PASO 5: Encontrar contornos y filtrar por características de placa ===
    print("   🔹 Buscando candidatos a placa...")
    contornos, _ = cv2.findContours(img_limpia, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # Crear máscara para la región de placa
    mascara_placa = np.zeros_like(img_gris)

    candidatos = []
    for cnt in contornos:
        area = cv2.contourArea(cnt)
        if area < 2000:  # Muy pequeño
            continue

        (x, y, w, h) = cv2.boundingRect(cnt)
        if h == 0:
            continue

        aspect_ratio = w / float(h)

        # Filtro muy permisivo: solo buscar cosas rectangulares horizontales
        if aspect_ratio > 1.5 and aspect_ratio < 6.0 and area > 5000:
            candidatos.append({
                'contorno': cnt,
                'area': area,
                'ar': aspect_ratio,
                'x': x, 'y': y, 'w': w, 'h': h
            })

    # Tomar el candidato con mejor área y aspect ratio
    if candidatos:
        # Ordenar por score combinado
        for c in candidatos:
            score = c['area'] * (1.0 / abs(c['ar'] - 3.5))  # Preferir AR cercano a 3.5
            c['score'] = score

        candidatos.sort(key=lambda x: x['score'], reverse=True)
        mejor = candidatos[0]

        print(f"   ✓ Región candidata encontrada:")
        print(f"     - Área: {mejor['area']:.0f}, AR: {mejor['ar']:.2f}")
        print(f"     - Posición: ({mejor['x']}, {mejor['y']}), Tamaño: {mejor['w']}x{mejor['h']}")

        # Expandir un poco la región para capturar toda la placa
        margen = 10
        x1 = max(0, mejor['x'] - margen)
        y1 = max(0, mejor['y'] - margen)
        x2 = min(img_gris.shape[1], mejor['x'] + mejor['w'] + margen)
        y2 = min(img_gris.shape[0], mejor['y'] + mejor['h'] + margen)

        # Crear máscara de la región
        mascara_placa[y1:y2, x1:x2] = 255
    else:
        print(f"   ⚠️ No se encontró región candidata - usando toda la imagen")
        mascara_placa = np.ones_like(img_gris) * 255

    todas_regiones_placa.append(mascara_placa)

    # === PASO 6: Aplicar Canny SOLO en la región de placa ===
    print("   🔹 Aplicando Canny en región de interés...")
    img_blur = cv2.GaussianBlur(img_gris, (5, 5), 0)
    img_canny = cv2.Canny(img_blur, 100, 200)

    # Aplicar máscara: solo bordes dentro de la región de placa
    img_canny_filtrado = cv2.bitwise_and(img_canny, img_canny, mask=mascara_placa)

    todas_imagenes_binarias.append(img_canny_filtrado)

    print(f"   ✓ Procesamiento completado")

print(f"\n{'='*70}")
print(f"✅ Detección de regiones completada")
print(f"{'='*70}\n")

# === VISUALIZACIÓN ===
fig, axes = plt.subplots(num_placas, 5, figsize=(25, 5 * num_placas))

if num_placas == 1:
    axes = axes.reshape(1, -1)

for idx in range(num_placas):
    img_gris = cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2GRAY)
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
    img_clahe = clahe.apply(img_gris)
    blur = cv2.GaussianBlur(img_clahe, (5, 5), 0)
    _, img_otsu = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

    # Original
    axes[idx, 0].imshow(cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2RGB))
    axes[idx, 0].set_title(f'Placa {idx + 1} - Original', fontsize=10, fontweight='bold')
    axes[idx, 0].axis('off')

    # CLAHE
    axes[idx, 1].imshow(img_clahe, cmap='gray')
    axes[idx, 1].set_title('CLAHE', fontsize=10)
    axes[idx, 1].axis('off')

    # Otsu
    axes[idx, 2].imshow(img_otsu, cmap='gray')
    axes[idx, 2].set_title('Otsu (regiones claras)', fontsize=10)
    axes[idx, 2].axis('off')

    # Máscara de región
    axes[idx, 3].imshow(todas_regiones_placa[idx], cmap='gray')
    axes[idx, 3].set_title('Región de Placa', fontsize=10, color='blue', fontweight='bold')
    axes[idx, 3].axis('off')

    # Canny filtrado
    axes[idx, 4].imshow(todas_imagenes_binarias[idx], cmap='gray')
    axes[idx, 4].set_title('Canny FILTRADO', fontsize=10, color='green', fontweight='bold')
    axes[idx, 4].axis('off')

plt.tight_layout()
plt.show()

print("✅ Ahora la columna 'Canny FILTRADO' debería mostrar SOLO bordes de la placa")
======================================================================
🎯 DETECCIÓN INTELIGENTE DE REGIÓN DE PLACA
======================================================================

──────────────────────────────────────────────────────────────────────
📋 PLACA 1/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ⚠️ No se encontró región candidata - usando toda la imagen
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 2/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ✓ Región candidata encontrada:
     - Área: 64020, AR: 2.82
     - Posición: (48, 276), Tamaño: 686x243
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 3/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ⚠️ No se encontró región candidata - usando toda la imagen
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 4/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ✓ Región candidata encontrada:
     - Área: 11132, AR: 4.15
     - Posición: (472, 987), Tamaño: 328x79
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 5/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ✓ Región candidata encontrada:
     - Área: 8704, AR: 5.07
     - Posición: (461, 488), Tamaño: 289x57
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 6/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ✓ Región candidata encontrada:
     - Área: 81550, AR: 3.66
     - Posición: (0, 406), Tamaño: 710x194
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 7/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ⚠️ No se encontró región candidata - usando toda la imagen
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 8/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ✓ Región candidata encontrada:
     - Área: 223533, AR: 2.08
     - Posición: (0, 0), Tamaño: 800x385
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 9/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ✓ Región candidata encontrada:
     - Área: 194384, AR: 1.78
     - Posición: (0, 0), Tamaño: 747x419
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 10/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ✓ Región candidata encontrada:
     - Área: 153211, AR: 2.87
     - Posición: (0, 485), Tamaño: 800x279
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 11/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ✓ Región candidata encontrada:
     - Área: 90548, AR: 3.46
     - Posición: (0, 439), Tamaño: 800x231
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 12/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ⚠️ No se encontró región candidata - usando toda la imagen
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 13/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ✓ Región candidata encontrada:
     - Área: 40505, AR: 2.80
     - Posición: (0, 344), Tamaño: 718x256
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 14/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ✓ Región candidata encontrada:
     - Área: 222240, AR: 2.29
     - Posición: (0, 716), Tamaño: 800x350
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 15/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ⚠️ No se encontró región candidata - usando toda la imagen
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 16/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ✓ Región candidata encontrada:
     - Área: 133722, AR: 2.74
     - Posición: (0, 0), Tamaño: 800x292
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 17/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ✓ Región candidata encontrada:
     - Área: 40505, AR: 2.80
     - Posición: (0, 344), Tamaño: 718x256
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 18/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ✓ Región candidata encontrada:
     - Área: 223533, AR: 2.08
     - Posición: (0, 0), Tamaño: 800x385
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

──────────────────────────────────────────────────────────────────────
📋 PLACA 19/19
──────────────────────────────────────────────────────────────────────
   🔹 Ecualización adaptativa...
   🔹 Binarización de Otsu (detectar región clara de placa)...
   🔹 Limpieza morfológica...
   🔹 Buscando candidatos a placa...
   ✓ Región candidata encontrada:
     - Área: 235416, AR: 1.90
     - Posición: (0, 645), Tamaño: 800x421
   🔹 Aplicando Canny en región de interés...
   ✓ Procesamiento completado

======================================================================
✅ Detección de regiones completada
======================================================================

No description has been provided for this image
✅ Ahora la columna 'Canny FILTRADO' debería mostrar SOLO bordes de la placa
In [11]:
# CELDA 11:
# ===================================================================
# FINAL: COMBINACIÓN DE OPERACIONES MORFOLÓGICAS
# ===================================================================


num_placas = len(imagenes_originales_p2b)

print("="*70)
print("🔬 PIPELINE MORFOLÓGICO COMPLETO")
print("="*70)
print("Combinando: Apertura → Top-Hat → Gradiente → Cierre → Dilatación")
print("="*70)

todas_dilataciones_binarias = []
imagenes_debug = []  # Para ver pasos intermedios

for idx in range(num_placas):
    print(f"\n{'─'*70}")
    print(f"📋 PLACA {idx + 1}/{num_placas}")
    print(f"{'─'*70}")

    img_original = imagenes_originales_p2b[idx].copy()

    # PASO 1: Escala de grises
    img_gris = cv2.cvtColor(img_original, cv2.COLOR_BGR2GRAY)

    # PASO 2: Suavizado bilateral (preserva bordes)
    img_blur = cv2.bilateralFilter(img_gris, 11, 17, 17)
    print(f"   ✓ Filtro bilateral aplicado")

    # PASO 3: APERTURA para eliminar ruido pequeño ANTES de Top-Hat
    kernel_apertura = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    img_apertura = cv2.morphologyEx(img_blur, cv2.MORPH_OPEN, kernel_apertura, iterations=1)
    print(f"   ✓ Apertura (limpieza de ruido)")

    # PASO 4: TOP-HAT sobre imagen limpia
    kernel_tophat = cv2.getStructuringElement(cv2.MORPH_RECT, (35, 18))
    img_tophat = cv2.morphologyEx(img_apertura, cv2.MORPH_TOPHAT, kernel_tophat)
    print(f"   ✓ Top-Hat (kernel 35x18)")

    # PASO 5: GRADIENTE MORFOLÓGICO para resaltar bordes
    kernel_gradiente = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    img_dilatada_temp = cv2.dilate(img_tophat, kernel_gradiente, iterations=1)
    img_erosionada_temp = cv2.erode(img_tophat, kernel_gradiente, iterations=1)
    img_gradiente = cv2.subtract(img_dilatada_temp, img_erosionada_temp)
    print(f"   ✓ Gradiente morfológico")

    # PASO 6: Combinar Top-Hat + Gradiente (da más información)
    img_combinada = cv2.addWeighted(img_tophat, 0.7, img_gradiente, 0.3, 0)

    # PASO 7: Binarización con Otsu
    _, img_binaria = cv2.threshold(img_combinada, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    print(f"   ✓ Binarización Otsu (umbral={_})")

    # PASO 8: CIERRE agresivo para unir caracteres
    kernel_cierre = cv2.getStructuringElement(cv2.MORPH_RECT, (30, 5))
    img_cerrada = cv2.morphologyEx(img_binaria, cv2.MORPH_CLOSE, kernel_cierre, iterations=3)
    print(f"   ✓ Cierre (kernel 30x5, 3 iter)")

    # PASO 9: APERTURA para eliminar ruido conectado
    kernel_limpieza = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
    img_limpia = cv2.morphologyEx(img_cerrada, cv2.MORPH_OPEN, kernel_limpieza, iterations=1)
    print(f"   ✓ Apertura (limpieza final)")

    # PASO 10: DILATACIÓN final para expandir región de placa
    kernel_dilatacion = cv2.getStructuringElement(cv2.MORPH_RECT, (20, 8))
    img_dilatada = cv2.dilate(img_limpia, kernel_dilatacion, iterations=2)
    print(f"   ✓ Dilatación (kernel 20x8, 2 iter)")

    # PASO 11: Filtrar contornos por área mínima
    contornos_temp, _ = cv2.findContours(img_dilatada, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    img_final = np.zeros_like(img_dilatada)

    for cnt in contornos_temp:
        area = cv2.contourArea(cnt)
        if area > 2000:  # Solo regiones significativas
            cv2.drawContours(img_final, [cnt], -1, 255, thickness=cv2.FILLED)

    print(f"   ✓ Filtrado de contornos pequeños")

    todas_dilataciones_binarias.append(img_final)

    # Guardar imágenes intermedias para debug
    imagenes_debug.append({
        'tophat': img_tophat,
        'gradiente': img_gradiente,
        'combinada': img_combinada,
        'binaria': img_binaria,
        'cerrada': img_cerrada,
        'final': img_final
    })

    # Diagnóstico
    contornos_final, _ = cv2.findContours(img_final, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    print(f"   📊 Contornos finales: {len(contornos_final)}")

    if len(contornos_final) > 0:
        cnt_mayor = max(contornos_final, key=cv2.contourArea)
        area_mayor = cv2.contourArea(cnt_mayor)
        (x, y, w, h) = cv2.boundingRect(cnt_mayor)
        ar = w / float(h) if h > 0 else 0
        print(f"   📊 Mayor: Área={area_mayor:.0f}, AR={ar:.2f}, {w}x{h}")

print(f"\n{'='*70}")
print("✅ Pipeline morfológico completado")
print(f"{'='*70}\n")

# VISUALIZACIÓN EXTENDIDA (6 columnas)
fig, axes = plt.subplots(num_placas, 6, figsize=(30, 5 * num_placas))

if num_placas == 1:
    axes = axes.reshape(1, -1)

for idx in range(num_placas):
    # Original
    axes[idx, 0].imshow(cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2RGB))
    axes[idx, 0].set_title(f'Placa {idx + 1}', fontsize=10, fontweight='bold')
    axes[idx, 0].axis('off')

    # Top-Hat
    axes[idx, 1].imshow(imagenes_debug[idx]['tophat'], cmap='gray')
    axes[idx, 1].set_title('Top-Hat', fontsize=10)
    axes[idx, 1].axis('off')

    # Gradiente
    axes[idx, 2].imshow(imagenes_debug[idx]['gradiente'], cmap='gray')
    axes[idx, 2].set_title('Gradiente', fontsize=10)
    axes[idx, 2].axis('off')

    # Combinada
    axes[idx, 3].imshow(imagenes_debug[idx]['combinada'], cmap='gray')
    axes[idx, 3].set_title('Combinada', fontsize=10, color='blue')
    axes[idx, 3].axis('off')

    # Después de cierre
    axes[idx, 4].imshow(imagenes_debug[idx]['cerrada'], cmap='gray')
    axes[idx, 4].set_title('Cierre', fontsize=10)
    axes[idx, 4].axis('off')

    # Final
    axes[idx, 5].imshow(imagenes_debug[idx]['final'], cmap='gray')
    axes[idx, 5].set_title('RESULTADO', fontsize=10, color='green', fontweight='bold')
    axes[idx, 5].axis('off')

plt.tight_layout()
plt.show()

print("\n💡 Pipeline completo:")
print("   1. Bilateral Filter (reduce ruido)")
print("   2. Apertura (elimina puntos aislados)")
print("   3. Top-Hat (resalta placas claras)")
print("   4. Gradiente (resalta bordes)")
print("   5. Combinación (más información)")
print("   6. Cierre (une caracteres)")
print("   7. Apertura (limpia ruido conectado)")
print("   8. Dilatación (expande región final)")
======================================================================
🔬 PIPELINE MORFOLÓGICO COMPLETO
======================================================================
Combinando: Apertura → Top-Hat → Gradiente → Cierre → Dilatación
======================================================================

──────────────────────────────────────────────────────────────────────
📋 PLACA 1/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=32.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 5
   📊 Mayor: Área=342135, AR=1.63, 800x491

──────────────────────────────────────────────────────────────────────
📋 PLACA 2/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=42.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 8
   📊 Mayor: Área=30228, AR=2.37, 372x157

──────────────────────────────────────────────────────────────────────
📋 PLACA 3/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=34.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 8
   📊 Mayor: Área=121235, AR=2.08, 800x385

──────────────────────────────────────────────────────────────────────
📋 PLACA 4/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=40.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 4
   📊 Mayor: Área=500662, AR=0.75, 800x1066

──────────────────────────────────────────────────────────────────────
📋 PLACA 5/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=35.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 3
   📊 Mayor: Área=339424, AR=1.73, 800x463

──────────────────────────────────────────────────────────────────────
📋 PLACA 6/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=46.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 3
   📊 Mayor: Área=15934, AR=1.92, 179x93

──────────────────────────────────────────────────────────────────────
📋 PLACA 7/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=34.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 3
   📊 Mayor: Área=294962, AR=1.02, 757x744

──────────────────────────────────────────────────────────────────────
📋 PLACA 8/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=36.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 16
   📊 Mayor: Área=81797, AR=0.85, 430x505

──────────────────────────────────────────────────────────────────────
📋 PLACA 9/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=41.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 3
   📊 Mayor: Área=89038, AR=3.53, 684x194

──────────────────────────────────────────────────────────────────────
📋 PLACA 10/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=32.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 13
   📊 Mayor: Área=23109, AR=8.25, 800x97

──────────────────────────────────────────────────────────────────────
📋 PLACA 11/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=45.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 5
   📊 Mayor: Área=65216, AR=2.81, 595x212

──────────────────────────────────────────────────────────────────────
📋 PLACA 12/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=51.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 6
   📊 Mayor: Área=103242, AR=4.42, 800x181

──────────────────────────────────────────────────────────────────────
📋 PLACA 13/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=42.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 11
   📊 Mayor: Área=39642, AR=3.26, 456x140

──────────────────────────────────────────────────────────────────────
📋 PLACA 14/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=34.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 8
   📊 Mayor: Área=168199, AR=2.38, 682x287

──────────────────────────────────────────────────────────────────────
📋 PLACA 15/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=34.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 3
   📊 Mayor: Área=294962, AR=1.02, 757x744

──────────────────────────────────────────────────────────────────────
📋 PLACA 16/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=38.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 9
   📊 Mayor: Área=73124, AR=2.81, 585x208

──────────────────────────────────────────────────────────────────────
📋 PLACA 17/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=42.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 11
   📊 Mayor: Área=39642, AR=3.26, 456x140

──────────────────────────────────────────────────────────────────────
📋 PLACA 18/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=36.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 16
   📊 Mayor: Área=81797, AR=0.85, 430x505

──────────────────────────────────────────────────────────────────────
📋 PLACA 19/19
──────────────────────────────────────────────────────────────────────
   ✓ Filtro bilateral aplicado
   ✓ Apertura (limpieza de ruido)
   ✓ Top-Hat (kernel 35x18)
   ✓ Gradiente morfológico
   ✓ Binarización Otsu (umbral=25.0)
   ✓ Cierre (kernel 30x5, 3 iter)
   ✓ Apertura (limpieza final)
   ✓ Dilatación (kernel 20x8, 2 iter)
   ✓ Filtrado de contornos pequeños
   📊 Contornos finales: 7
   📊 Mayor: Área=242986, AR=1.75, 800x456

======================================================================
✅ Pipeline morfológico completado
======================================================================

No description has been provided for this image
💡 Pipeline completo:
   1. Bilateral Filter (reduce ruido)
   2. Apertura (elimina puntos aislados)
   3. Top-Hat (resalta placas claras)
   4. Gradiente (resalta bordes)
   5. Combinación (más información)
   6. Cierre (une caracteres)
   7. Apertura (limpia ruido conectado)
   8. Dilatación (expande región final)
In [12]:
# CELDA 11:
# ===================================================================
# CELDA 5: FILTRADO CON ASPECT RATIO CORREGIDO
# ===================================================================

import numpy as np

# Filtros más permisivos basados en el diagnóstico
FILTRO_AREA_MIN = 18000  # Reducido de 30000 (Placa 1 tiene 29894)
FILTRO_ASPECT_RATIO_MIN = 1.9  # Más permisivo
FILTRO_ASPECT_RATIO_MAX = 4.0  # Ampliado para capturar Placa 1 (AR=8.01)
FILTRO_SOLIDEZ_MIN = 0.25  # Ligeramente más permisivo

num_placas = len(imagenes_originales_p2b)

print("="*70)
print("🔍 APLICANDO FILTROS PARA PLACAS HORIZONTALES")
print("="*70)
print(f"📋 Filtros configurados:")
print(f"   - Área mínima: {FILTRO_AREA_MIN}")
print(f"   - Aspect Ratio (ancho/alto): {FILTRO_ASPECT_RATIO_MIN} - {FILTRO_ASPECT_RATIO_MAX}")
print(f"   - Solidez mínima: {FILTRO_SOLIDEZ_MIN}")
print("="*70)

mejores_contornos_placas = []
resultados_filtrado = []

for idx in range(num_placas):
    print(f"\n{'─'*70}")
    print(f"📋 PLACA {idx + 1}/{num_placas}")
    print(f"{'─'*70}")

    contornos, _ = cv2.findContours(todas_dilataciones_binarias[idx].copy(),
                                    cv2.RETR_EXTERNAL,
                                    cv2.CHAIN_APPROX_SIMPLE)

    mejor_placa_contorno = None
    mejor_score = 0
    candidatos_pasaron = 0

    print(f"   📊 Total de contornos: {len(contornos)}")

    for contorno in contornos:
        area = cv2.contourArea(contorno)
        (x, y, w, h) = cv2.boundingRect(contorno)

        if h == 0 or w == 0:
            continue

        aspect_ratio = w / float(h)
        solidez = area / (w * h)

        # Filtros para placas HORIZONTALES
        if (area > FILTRO_AREA_MIN and
            aspect_ratio > FILTRO_ASPECT_RATIO_MIN and
            aspect_ratio < FILTRO_ASPECT_RATIO_MAX and
            solidez > FILTRO_SOLIDEZ_MIN):

            candidatos_pasaron += 1

            # Sistema de scoring
            score = (area / 100000) * 30
            aspect_ideal = 3.5
            aspect_diff = abs(aspect_ratio - aspect_ideal)
            score += max(0, (1 - aspect_diff/2) * 40)
            score += solidez * 30

            print(f"   ✓ Candidato #{candidatos_pasaron}:")
            print(f"     - Área: {area:.0f}, AR: {aspect_ratio:.2f}, Solidez: {solidez:.2f}, Score: {score:.1f}")

            if score > mejor_score:
                mejor_score = score
                mejor_placa_contorno = contorno

    mejores_contornos_placas.append(mejor_placa_contorno)

    if mejor_placa_contorno is not None:
        area_final = cv2.contourArea(mejor_placa_contorno)
        print(f"\n   ✅ Placa detectada - Área: {area_final:.0f}, Score: {mejor_score:.1f}")
        resultados_filtrado.append({
            'placa': idx + 1,
            'estado': 'ÉXITO',
            'area': area_final,
            'score': mejor_score,
            'validos': candidatos_pasaron
        })
    else:
        print(f"\n   ❌ No detectada - {candidatos_pasaron} candidatos pasaron filtros")
        resultados_filtrado.append({
            'placa': idx + 1,
            'estado': 'FALLO',
            'area': 0,
            'score': 0,
            'validos': candidatos_pasaron
        })

# Resumen
print(f"\n{'='*70}")
print(f"📊 RESUMEN")
print(f"{'='*70}")
placas_exitosas = sum(1 for r in resultados_filtrado if r['estado'] == 'ÉXITO')
print(f"✅ Detectadas: {placas_exitosas}/{num_placas}")
print(f"❌ No detectadas: {num_placas - placas_exitosas}/{num_placas}")

# Visualización
fig, axes = plt.subplots(num_placas, 2, figsize=(16, 8 * num_placas))
if num_placas == 1:
    axes = axes.reshape(1, -1)

for idx in range(num_placas):
    axes[idx, 0].imshow(cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2RGB))
    axes[idx, 0].set_title(f'Placa {idx + 1} - Original', fontsize=12)
    axes[idx, 0].axis('off')

    imagen_resultado = imagenes_originales_p2b[idx].copy()

    if mejores_contornos_placas[idx] is not None:
        cv2.drawContours(imagen_resultado, [mejores_contornos_placas[idx]], -1, (0, 255, 0), 3)
        (x, y, w, h) = cv2.boundingRect(mejores_contornos_placas[idx])
        cv2.rectangle(imagen_resultado, (x, y), (x + w, y + h), (0, 255, 0), 2)
        titulo = f'[OK] Detectada (Score: {resultados_filtrado[idx]["score"]:.1f})'
        color = 'green'
    else:
        titulo = '[X] No Detectada'
        color = 'red'

    axes[idx, 1].imshow(cv2.cvtColor(imagen_resultado, cv2.COLOR_BGR2RGB))
    axes[idx, 1].set_title(titulo, fontsize=12, color=color, fontweight='bold')
    axes[idx, 1].axis('off')

plt.tight_layout()
plt.show()
======================================================================
🔍 APLICANDO FILTROS PARA PLACAS HORIZONTALES
======================================================================
📋 Filtros configurados:
   - Área mínima: 18000
   - Aspect Ratio (ancho/alto): 1.9 - 4.0
   - Solidez mínima: 0.25
======================================================================

──────────────────────────────────────────────────────────────────────
📋 PLACA 1/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 5
   ✓ Candidato #1:
     - Área: 180102, AR: 2.18, Solidez: 0.61, Score: 86.0

   ✅ Placa detectada - Área: 180102, Score: 86.0

──────────────────────────────────────────────────────────────────────
📋 PLACA 2/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 8
   ✓ Candidato #1:
     - Área: 30228, AR: 2.37, Solidez: 0.52, Score: 42.0

   ✅ Placa detectada - Área: 30228, Score: 42.0

──────────────────────────────────────────────────────────────────────
📋 PLACA 3/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 8
   ✓ Candidato #1:
     - Área: 21862, AR: 3.20, Solidez: 0.74, Score: 62.8
   ✓ Candidato #2:
     - Área: 121235, AR: 2.08, Solidez: 0.39, Score: 59.7

   ✅ Placa detectada - Área: 21862, Score: 62.8

──────────────────────────────────────────────────────────────────────
📋 PLACA 4/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 4

   ❌ No detectada - 0 candidatos pasaron filtros

──────────────────────────────────────────────────────────────────────
📋 PLACA 5/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 3
   ✓ Candidato #1:
     - Área: 23053, AR: 3.87, Solidez: 0.47, Score: 53.6

   ✅ Placa detectada - Área: 23053, Score: 53.6

──────────────────────────────────────────────────────────────────────
📋 PLACA 6/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 3

   ❌ No detectada - 0 candidatos pasaron filtros

──────────────────────────────────────────────────────────────────────
📋 PLACA 7/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 3

   ❌ No detectada - 0 candidatos pasaron filtros

──────────────────────────────────────────────────────────────────────
📋 PLACA 8/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 16

   ❌ No detectada - 0 candidatos pasaron filtros

──────────────────────────────────────────────────────────────────────
📋 PLACA 9/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 3
   ✓ Candidato #1:
     - Área: 89038, AR: 3.53, Solidez: 0.67, Score: 86.3

   ✅ Placa detectada - Área: 89038, Score: 86.3

──────────────────────────────────────────────────────────────────────
📋 PLACA 10/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 13
   ✓ Candidato #1:
     - Área: 19966, AR: 2.04, Solidez: 0.40, Score: 28.8

   ✅ Placa detectada - Área: 19966, Score: 28.8

──────────────────────────────────────────────────────────────────────
📋 PLACA 11/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 5
   ✓ Candidato #1:
     - Área: 65216, AR: 2.81, Solidez: 0.52, Score: 61.2
   ✓ Candidato #2:
     - Área: 36842, AR: 3.12, Solidez: 0.45, Score: 57.2

   ✅ Placa detectada - Área: 65216, Score: 61.2

──────────────────────────────────────────────────────────────────────
📋 PLACA 12/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 6
   ✓ Candidato #1:
     - Área: 28404, AR: 1.90, Solidez: 0.31, Score: 25.8

   ✅ Placa detectada - Área: 28404, Score: 25.8

──────────────────────────────────────────────────────────────────────
📋 PLACA 13/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 11
   ✓ Candidato #1:
     - Área: 39642, AR: 3.26, Solidez: 0.62, Score: 65.7

   ✅ Placa detectada - Área: 39642, Score: 65.7

──────────────────────────────────────────────────────────────────────
📋 PLACA 14/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 8
   ✓ Candidato #1:
     - Área: 168199, AR: 2.38, Solidez: 0.86, Score: 93.8
   ✓ Candidato #2:
     - Área: 78872, AR: 2.71, Solidez: 0.44, Score: 60.9

   ✅ Placa detectada - Área: 168199, Score: 93.8

──────────────────────────────────────────────────────────────────────
📋 PLACA 15/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 3

   ❌ No detectada - 0 candidatos pasaron filtros

──────────────────────────────────────────────────────────────────────
📋 PLACA 16/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 9
   ✓ Candidato #1:
     - Área: 73124, AR: 2.81, Solidez: 0.60, Score: 66.2

   ✅ Placa detectada - Área: 73124, Score: 66.2

──────────────────────────────────────────────────────────────────────
📋 PLACA 17/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 11
   ✓ Candidato #1:
     - Área: 39642, AR: 3.26, Solidez: 0.62, Score: 65.7

   ✅ Placa detectada - Área: 39642, Score: 65.7

──────────────────────────────────────────────────────────────────────
📋 PLACA 18/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 16

   ❌ No detectada - 0 candidatos pasaron filtros

──────────────────────────────────────────────────────────────────────
📋 PLACA 19/19
──────────────────────────────────────────────────────────────────────
   📊 Total de contornos: 7
   ✓ Candidato #1:
     - Área: 81900, AR: 3.86, Solidez: 0.49, Score: 72.1
   ✓ Candidato #2:
     - Área: 32714, AR: 3.13, Solidez: 0.36, Score: 53.1

   ✅ Placa detectada - Área: 81900, Score: 72.1

======================================================================
📊 RESUMEN
======================================================================
✅ Detectadas: 13/19
❌ No detectadas: 6/19
No description has been provided for this image
In [13]:
# CELDA 12:
# ===================================================================
# RECORTE DE PLACAS DETECTADAS
# ===================================================================

num_placas = len(imagenes_originales_p2b)

print("="*70)
print("📸 RECORTANDO PLACAS DETECTADAS")
print("="*70)

# Listas para almacenar recortes
todas_placas_recortadas_color = []
todas_placas_recortadas_gris = []  # CAMBIADO: gris en lugar de binaria

indices_exitosos = []

for idx in range(num_placas):
    if mejores_contornos_placas[idx] is not None:
        (x, y, w, h) = cv2.boundingRect(mejores_contornos_placas[idx])

        # Recortar de la imagen ORIGINAL en color
        placa_recortada_color = imagenes_originales_p2b[idx][y:y+h, x:x+w]

        # Recortar en ESCALA DE GRISES (no binaria)
        img_gris = cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2GRAY)
        placa_recortada_gris = img_gris[y:y+h, x:x+w]

        todas_placas_recortadas_color.append(placa_recortada_color)
        todas_placas_recortadas_gris.append(placa_recortada_gris)
        indices_exitosos.append(idx)

        print(f"✅ Placa {idx + 1}: Recortada ({w}x{h} px)")
    else:
        # Si no se detectó, agregar None
        todas_placas_recortadas_color.append(None)
        todas_placas_recortadas_gris.append(None)
        print(f"❌ Placa {idx + 1}: No detectada")

num_exitosas = len(indices_exitosos)

print(f"\n{'='*70}")
print(f"📊 {num_exitosas} de {num_placas} placas recortadas exitosamente")
print(f"{'='*70}\n")

# Visualización
if num_exitosas > 0:
    fig, axes = plt.subplots(num_exitosas, 3, figsize=(18, 6 * num_exitosas))

    if num_exitosas == 1:
        axes = axes.reshape(1, -1)

    for i, idx in enumerate(indices_exitosos):
        # Imagen con rectángulo
        imagen_con_placa = imagenes_originales_p2b[idx].copy()
        (x, y, w, h) = cv2.boundingRect(mejores_contornos_placas[idx])
        cv2.rectangle(imagen_con_placa, (x, y), (x + w, y + h), (0, 255, 0), 3)

        axes[i, 0].imshow(cv2.cvtColor(imagen_con_placa, cv2.COLOR_BGR2RGB))
        axes[i, 0].set_title(f'Placa {idx + 1} - Detectada', fontweight='bold')
        axes[i, 0].axis('off')

        axes[i, 1].imshow(cv2.cvtColor(todas_placas_recortadas_color[idx], cv2.COLOR_BGR2RGB))
        axes[i, 1].set_title('Recorte (Color)', fontweight='bold')
        axes[i, 1].axis('off')

        axes[i, 2].imshow(todas_placas_recortadas_gris[idx], cmap='gray')
        axes[i, 2].set_title('Recorte (Grises)', fontweight='bold')
        axes[i, 2].axis('off')

    plt.tight_layout()
    plt.show()

    print("✅ Visualización completada")
else:
    print("❌ No hay placas exitosas para visualizar")
======================================================================
📸 RECORTANDO PLACAS DETECTADAS
======================================================================
✅ Placa 1: Recortada (800x367 px)
✅ Placa 2: Recortada (372x157 px)
✅ Placa 3: Recortada (307x96 px)
❌ Placa 4: No detectada
✅ Placa 5: Recortada (437x113 px)
❌ Placa 6: No detectada
❌ Placa 7: No detectada
❌ Placa 8: No detectada
✅ Placa 9: Recortada (684x194 px)
✅ Placa 10: Recortada (318x156 px)
✅ Placa 11: Recortada (595x212 px)
✅ Placa 12: Recortada (421x221 px)
✅ Placa 13: Recortada (456x140 px)
✅ Placa 14: Recortada (682x287 px)
❌ Placa 15: No detectada
✅ Placa 16: Recortada (585x208 px)
✅ Placa 17: Recortada (456x140 px)
❌ Placa 18: No detectada
✅ Placa 19: Recortada (800x207 px)

======================================================================
📊 13 de 19 placas recortadas exitosamente
======================================================================

No description has been provided for this image
✅ Visualización completada
In [14]:
# CELDA 13:
# ===================================================================
# ROTACIÓN PRECISA (BATCH) - VERSIÓN DEFINITIVA
# ===================================================================

if 'todas_placas_recortadas_color' not in locals() or 'todas_placas_recortadas_gris' not in locals():
    print("❌ ERROR: No se encontraron las placas recortadas.")
    print("   Por favor, ejecuta primero la CELDA 6.")
else:
    num_placas = len(imagenes_originales_p2b)

    print("="*70)
    print("🔄 CORRECCIÓN DE ROTACIÓN - TODAS LAS PLACAS")
    print("="*70)

    todas_placas_enderezadas = []

    placas_enderezadas_exitosas = 0
    placas_sin_enderezar = 0

    for idx in range(num_placas):
        print(f"\n{'─'*70}")
        print(f"📋 PLACA {idx + 1}/{num_placas}")

        if todas_placas_recortadas_gris[idx] is not None:

            try:
                placa_gris = todas_placas_recortadas_gris[idx]
                (h, w) = placa_gris.shape

                print(f"   📐 Dimensiones: {w}x{h}")

                # ESTRATEGIA SIMPLE: Verificar si está horizontal o vertical
                # Las placas deben ser más anchas que altas
                if w < h:
                    # Está vertical - rotar 90 grados
                    placa_enderezada = cv2.rotate(placa_gris, cv2.ROTATE_90_CLOCKWISE)
                    print(f"   🔄 Placa vertical detectada - rotando 90°")
                    placas_enderezadas_exitosas += 1
                else:
                    # Ya está horizontal - no rotar
                    placa_enderezada = placa_gris.copy()
                    print(f"   ✓ Placa ya está horizontal")
                    placas_enderezadas_exitosas += 1

                todas_placas_enderezadas.append(placa_enderezada)

                (h_final, w_final) = placa_enderezada.shape
                print(f"   📊 Dimensiones finales: {w_final}x{h_final}")

            except Exception as e:
                todas_placas_enderezadas.append(None)
                print(f"   ❌ Error: {str(e)}")
                placas_sin_enderezar += 1
        else:
            todas_placas_enderezadas.append(None)
            print(f"   ❌ No hay placa recortada")
            placas_sin_enderezar += 1

    print(f"\n{'='*70}")
    print(f"📊 RESUMEN")
    print(f"{'='*70}")
    print(f"✅ Placas procesadas: {placas_enderezadas_exitosas}/{num_placas}")
    print(f"❌ Placas sin procesar: {placas_sin_enderezar}/{num_placas}")
    print(f"{'='*70}\n")

    # VISUALIZACIÓN
    if placas_enderezadas_exitosas > 0:
        print("📊 Mostrando comparación antes/después...\n")

        placas_validas = sum(1 for p in todas_placas_enderezadas if p is not None)

        fig, axes = plt.subplots(placas_validas, 2, figsize=(14, 6 * placas_validas))

        if placas_validas == 1:
            axes = axes.reshape(1, -1)

        fila_actual = 0
        for idx in range(num_placas):
            if todas_placas_enderezadas[idx] is not None:
                axes[fila_actual, 0].imshow(todas_placas_recortadas_gris[idx], cmap='gray')
                axes[fila_actual, 0].set_title(f'Placa {idx + 1} - Recortada',
                                              fontsize=12, fontweight='bold')
                axes[fila_actual, 0].axis('off')

                axes[fila_actual, 1].imshow(todas_placas_enderezadas[idx], cmap='gray')
                axes[fila_actual, 1].set_title('Placa Procesada',
                                              fontsize=12, fontweight='bold', color='green')
                axes[fila_actual, 1].axis('off')

                fila_actual += 1

        plt.tight_layout()
        plt.show()

        print("✅ Visualización completada")
    else:
        print("⚠️  No hay placas procesadas")

    print("\n✅ Procesamiento completado")
======================================================================
🔄 CORRECCIÓN DE ROTACIÓN - TODAS LAS PLACAS
======================================================================

──────────────────────────────────────────────────────────────────────
📋 PLACA 1/19
   📐 Dimensiones: 800x367
   ✓ Placa ya está horizontal
   📊 Dimensiones finales: 800x367

──────────────────────────────────────────────────────────────────────
📋 PLACA 2/19
   📐 Dimensiones: 372x157
   ✓ Placa ya está horizontal
   📊 Dimensiones finales: 372x157

──────────────────────────────────────────────────────────────────────
📋 PLACA 3/19
   📐 Dimensiones: 307x96
   ✓ Placa ya está horizontal
   📊 Dimensiones finales: 307x96

──────────────────────────────────────────────────────────────────────
📋 PLACA 4/19
   ❌ No hay placa recortada

──────────────────────────────────────────────────────────────────────
📋 PLACA 5/19
   📐 Dimensiones: 437x113
   ✓ Placa ya está horizontal
   📊 Dimensiones finales: 437x113

──────────────────────────────────────────────────────────────────────
📋 PLACA 6/19
   ❌ No hay placa recortada

──────────────────────────────────────────────────────────────────────
📋 PLACA 7/19
   ❌ No hay placa recortada

──────────────────────────────────────────────────────────────────────
📋 PLACA 8/19
   ❌ No hay placa recortada

──────────────────────────────────────────────────────────────────────
📋 PLACA 9/19
   📐 Dimensiones: 684x194
   ✓ Placa ya está horizontal
   📊 Dimensiones finales: 684x194

──────────────────────────────────────────────────────────────────────
📋 PLACA 10/19
   📐 Dimensiones: 318x156
   ✓ Placa ya está horizontal
   📊 Dimensiones finales: 318x156

──────────────────────────────────────────────────────────────────────
📋 PLACA 11/19
   📐 Dimensiones: 595x212
   ✓ Placa ya está horizontal
   📊 Dimensiones finales: 595x212

──────────────────────────────────────────────────────────────────────
📋 PLACA 12/19
   📐 Dimensiones: 421x221
   ✓ Placa ya está horizontal
   📊 Dimensiones finales: 421x221

──────────────────────────────────────────────────────────────────────
📋 PLACA 13/19
   📐 Dimensiones: 456x140
   ✓ Placa ya está horizontal
   📊 Dimensiones finales: 456x140

──────────────────────────────────────────────────────────────────────
📋 PLACA 14/19
   📐 Dimensiones: 682x287
   ✓ Placa ya está horizontal
   📊 Dimensiones finales: 682x287

──────────────────────────────────────────────────────────────────────
📋 PLACA 15/19
   ❌ No hay placa recortada

──────────────────────────────────────────────────────────────────────
📋 PLACA 16/19
   📐 Dimensiones: 585x208
   ✓ Placa ya está horizontal
   📊 Dimensiones finales: 585x208

──────────────────────────────────────────────────────────────────────
📋 PLACA 17/19
   📐 Dimensiones: 456x140
   ✓ Placa ya está horizontal
   📊 Dimensiones finales: 456x140

──────────────────────────────────────────────────────────────────────
📋 PLACA 18/19
   ❌ No hay placa recortada

──────────────────────────────────────────────────────────────────────
📋 PLACA 19/19
   📐 Dimensiones: 800x207
   ✓ Placa ya está horizontal
   📊 Dimensiones finales: 800x207

======================================================================
📊 RESUMEN
======================================================================
✅ Placas procesadas: 13/19
❌ Placas sin procesar: 6/19
======================================================================

📊 Mostrando comparación antes/después...

No description has been provided for this image
✅ Visualización completada

✅ Procesamiento completado
In [15]:
# CELDA 14:
# ===================================================================
# DETECCIÓN Y EXTRACCIÓN DE PLACAS (BATCH)
# ===================================================================

# Verificar que existan las variables necesarias
if 'imagenes_originales_p2b' not in locals():
    print("❌ ERROR: No se encontraron las imágenes originales.")
    print("   Por favor, ejecuta primero las celdas anteriores.")
elif 'todas_dilataciones_binarias' not in locals():
    print("❌ ERROR: No se encontraron las máscaras morfológicas finales.")
    print("   Por favor, ejecuta primero la CELDA 4.")
else:
    print("="*70)
    print("🔍 DETECCIÓN Y EXTRACCIÓN DE PLACAS")
    print("="*70)

    # Parámetros de detección
    AREA_MIN = 15000
    AR_MIN = 1.5
    AR_MAX = 8.0
    SOLIDEZ_MIN = 0.20

    num_placas = len(imagenes_originales_p2b)
    todas_placas_recortadas_color = []
    todas_placas_recortadas_gris = []
    placas_detectadas = 0
    placas_no_detectadas = 0

    for idx in range(num_placas):
        print(f"\n{'─'*70}")
        print(f"📋 IMAGEN {idx + 1}/{num_placas}")
        print(f"{'─'*70}")

        img_original = imagenes_originales_p2b[idx]
        img_final = todas_dilataciones_binarias[idx]  # ✅ CORRECCIÓN AQUÍ

        # Encontrar contornos
        contornos, _ = cv2.findContours(img_final, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        print(f"   Total contornos: {len(contornos)}")

        candidatos = []

        for cnt in contornos:
            area = cv2.contourArea(cnt)
            if area < AREA_MIN:
                continue

            x, y, w, h = cv2.boundingRect(cnt)
            if h == 0:
                continue

            ar = w / float(h)
            if not (AR_MIN <= ar <= AR_MAX):
                continue

            solidez = area / float(w * h)
            if solidez < SOLIDEZ_MIN:
                continue

            # Calcular extent y score
            rect_area = w * h
            extent = area / rect_area
            score = area * (1.0 / (abs(ar - 3.5) + 0.5)) * solidez * extent

            candidatos.append({
                'bbox': (x, y, w, h),
                'area': area,
                'ar': ar,
                'solidez': solidez,
                'extent': extent,
                'score': score
            })

        candidatos.sort(key=lambda c: c['score'], reverse=True)
        print(f"   Candidatos válidos: {len(candidatos)}")

        # Extraer mejor placa si existe
        if len(candidatos) > 0:
            mejor = candidatos[0]
            x, y, w, h = mejor['bbox']

            print(f"   ✓ Placa detectada:")
            print(f"     - Área: {mejor['area']:.0f}")
            print(f"     - AR: {mejor['ar']:.2f}")
            print(f"     - Solidez: {mejor['solidez']:.2f}")

            # Extraer con margen
            margen = 10
            x1 = max(0, x - margen)
            y1 = max(0, y - margen)
            x2 = min(img_original.shape[1], x + w + margen)
            y2 = min(img_original.shape[0], y + h + margen)

            placa_color = img_original[y1:y2, x1:x2]
            placa_gris = cv2.cvtColor(placa_color, cv2.COLOR_BGR2GRAY)

            todas_placas_recortadas_color.append(placa_color)
            todas_placas_recortadas_gris.append(placa_gris)
            placas_detectadas += 1

            # Visualizar esta detección
            fig, axes = plt.subplots(1, 3, figsize=(20, 6))
            fig.suptitle(f"Placa {idx + 1} - Detección Exitosa",
                       fontsize=16, fontweight='bold', color='green')

            # Original
            axes[0].imshow(cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB))
            axes[0].set_title('Imagen Original', fontsize=14, fontweight='bold')
            axes[0].axis('off')

            # Máscara morfológica
            axes[1].imshow(img_final, cmap='gray')
            axes[1].set_title('Máscara Final', fontsize=14, fontweight='bold')
            axes[1].axis('off')

            # Placa extraída
            axes[2].imshow(cv2.cvtColor(placa_color, cv2.COLOR_BGR2RGB))
            axes[2].set_title(f"Placa Extraída (AR: {mejor['ar']:.2f})",
                            fontsize=14, fontweight='bold', color='green')
            axes[2].axis('off')

            plt.tight_layout()
            plt.show()

        else:
            print(f"   ❌ No se detectó placa válida")
            todas_placas_recortadas_color.append(None)
            todas_placas_recortadas_gris.append(None)
            placas_no_detectadas += 1

    # RESUMEN FINAL
    print(f"\n{'='*70}")
    print(f"📊 RESUMEN FINAL")
    print(f"{'='*70}")
    print(f"✅ Placas detectadas: {placas_detectadas}/{num_placas}")
    print(f"❌ Sin detección: {placas_no_detectadas}/{num_placas}")
    print(f"{'='*70}\n")

    print("✅ Variables guardadas:")
    print("   - todas_placas_recortadas_color")
    print("   - todas_placas_recortadas_gris")
    print("\n✅ CELDA 6 completada - Listo para CELDA 7 (Rotación)")
======================================================================
🔍 DETECCIÓN Y EXTRACCIÓN DE PLACAS
======================================================================

──────────────────────────────────────────────────────────────────────
📋 IMAGEN 1/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 5
   Candidatos válidos: 4
   ✓ Placa detectada:
     - Área: 342135
     - AR: 1.63
     - Solidez: 0.87
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 2/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 8
   Candidatos válidos: 1
   ✓ Placa detectada:
     - Área: 30228
     - AR: 2.37
     - Solidez: 0.52
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 3/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 8
   Candidatos válidos: 2
   ✓ Placa detectada:
     - Área: 21862
     - AR: 3.20
     - Solidez: 0.74
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 4/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 4
   Candidatos válidos: 0
   ❌ No se detectó placa válida

──────────────────────────────────────────────────────────────────────
📋 IMAGEN 5/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 3
   Candidatos válidos: 2
   ✓ Placa detectada:
     - Área: 339424
     - AR: 1.73
     - Solidez: 0.92
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 6/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 3
   Candidatos válidos: 1
   ✓ Placa detectada:
     - Área: 15934
     - AR: 1.92
     - Solidez: 0.96
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 7/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 3
   Candidatos válidos: 0
   ❌ No se detectó placa válida

──────────────────────────────────────────────────────────────────────
📋 IMAGEN 8/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 16
   Candidatos válidos: 1
   ✓ Placa detectada:
     - Área: 25280
     - AR: 1.84
     - Solidez: 0.51
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 9/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 3
   Candidatos válidos: 2
   ✓ Placa detectada:
     - Área: 89038
     - AR: 3.53
     - Solidez: 0.67
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 10/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 13
   Candidatos válidos: 2
   ✓ Placa detectada:
     - Área: 17972
     - AR: 3.29
     - Solidez: 0.72
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 11/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 5
   Candidatos válidos: 2
   ✓ Placa detectada:
     - Área: 65216
     - AR: 2.81
     - Solidez: 0.52
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 12/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 6
   Candidatos válidos: 2
   ✓ Placa detectada:
     - Área: 103242
     - AR: 4.42
     - Solidez: 0.71
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 13/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 11
   Candidatos válidos: 3
   ✓ Placa detectada:
     - Área: 39642
     - AR: 3.26
     - Solidez: 0.62
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 14/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 8
   Candidatos válidos: 2
   ✓ Placa detectada:
     - Área: 168199
     - AR: 2.38
     - Solidez: 0.86
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 15/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 3
   Candidatos válidos: 0
   ❌ No se detectó placa válida

──────────────────────────────────────────────────────────────────────
📋 IMAGEN 16/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 9
   Candidatos válidos: 1
   ✓ Placa detectada:
     - Área: 73124
     - AR: 2.81
     - Solidez: 0.60
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 17/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 11
   Candidatos válidos: 3
   ✓ Placa detectada:
     - Área: 39642
     - AR: 3.26
     - Solidez: 0.62
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 18/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 16
   Candidatos válidos: 1
   ✓ Placa detectada:
     - Área: 25280
     - AR: 1.84
     - Solidez: 0.51
No description has been provided for this image
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 19/19
──────────────────────────────────────────────────────────────────────
   Total contornos: 7
   Candidatos válidos: 3
   ✓ Placa detectada:
     - Área: 242986
     - AR: 1.75
     - Solidez: 0.67
No description has been provided for this image
======================================================================
📊 RESUMEN FINAL
======================================================================
✅ Placas detectadas: 16/19
❌ Sin detección: 3/19
======================================================================

✅ Variables guardadas:
   - todas_placas_recortadas_color
   - todas_placas_recortadas_gris

✅ CELDA 6 completada - Listo para CELDA 7 (Rotación)

Conclusiones¶


Resultados del Mini Proyecto¶

Este apéndice demostró exitosamente la aplicación secuencial de operaciones morfológicas en un problema real de detección de objetos. El pipeline implementado logró detectar y extraer placas vehiculares utilizando las seis operaciones morfológicas fundamentales estudiadas en el curso principal.


Desafíos Técnicos Encontrados¶

1. Ajuste de Parámetros de Kernels¶

El proceso de calibración de elementos estructurantes fue iterativo y requirió experimentación extensa:

  • Top-Hat kernel (35×18): Se probaron tamaños desde 15×10 hasta 50×25. Kernels pequeños no capturaban la región completa de la placa, mientras que kernels muy grandes incluían demasiado ruido del fondo.

  • Cierre morfológico (30×5): Este kernel rectangular fue crítico para conectar caracteres fragmentados. La proporción ancho:alto tuvo que ajustarse específicamente para placas horizontales. Valores como 20×3 dejaban huecos, mientras que 40×10 conectaban caracteres con ruido adyacente.

  • Dilatación final (20×8): El balance entre expandir suficiente la región sin incluir componentes no deseados requirió múltiples iteraciones.

2. Variabilidad en Dimensiones de Imágenes¶

Las imágenes del dataset presentaron diferentes resoluciones y aspect ratios:

  • Redimensionado: Se estandarizó a 800px de ancho, pero esto causó pérdida de detalle en imágenes de alta resolución originales.

  • Impacto en kernels: Los tamaños de kernel óptimos variaron según la escala de la imagen. Un kernel 30×5 efectivo en una imagen de 800px resultó insuficiente en imágenes más grandes.

3. Casos de Fallo¶

Placas no detectadas:

  • Imágenes con iluminación extremadamente baja donde el contraste entre placa y vehículo era mínimo
  • Placas parcialmente ocluidas o con ángulos de visión muy oblicuos
  • Fondos con regiones claras similares al tono de la placa (falsos positivos competitivos)

Detecciones incorrectas:

  • Regiones rectangulares del vehículo (parrillas, molduras) que cumplían criterios geométricos
  • Reflejos o elementos metálicos con alto contraste

4. Limitaciones del Pipeline Morfológico¶

  • Sensibilidad a iluminación: Las operaciones de umbralización (Otsu) fallaron en escenas de alto rango dinámico
  • Pérdida de información: El Top-Hat puede suprimir placas oscuras sobre fondos claros
  • Conectividad excesiva: El cierre morfológico ocasionalmente conectó la placa con componentes cercanos del vehículo

Aprendizajes Clave¶

  1. No existe un kernel universal: Los parámetros morfológicos son altamente dependientes del contexto y escala de la aplicación.

  2. Combinación estratégica: La secuencia Opening → Top-Hat → Gradient → Closing resultó más efectiva que operaciones individuales. Cada paso prepara la imagen para el siguiente.

  3. Trade-offs inevitables: Mayor conectividad (cierre agresivo) mejora la detección de placas fragmentadas pero aumenta falsos positivos. El balance depende de la aplicación específica.

  4. Preprocesamiento crítico: El filtro bilateral y la ecualización CLAHE fueron tan importantes como las operaciones morfológicas en sí.


Trabajo Futuro y Mejoras Propuestas¶

Mejoras Técnicas¶

  1. Kernels adaptativos:

    • Implementar selección automática de tamaño de kernel basado en la resolución de entrada
    • Utilizar múltiples kernels en paralelo y fusionar resultados
  2. Umbralización adaptativa:

    • Reemplazar Otsu global con umbralización adaptativa local
    • Implementar binarización robusta a variaciones de iluminación (algoritmo de Sauvola o Wolf)
  3. Pipeline híbrido:

    • Combinar morfología clásica con detección basada en gradientes (Canny, Sobel)
    • Implementar detector de regiones MSER (Maximally Stable Extremal Regions) como complemento
  4. Post-procesamiento geométrico:

    • Añadir corrección de perspectiva para placas oblicuas
    • Implementar verificación de patrones (texto horizontal esperado)

Extensiones del Proyecto¶

  1. Morfología en escala de grises:

    • Explorar operaciones morfológicas directamente en imágenes grises sin binarización
    • Implementar reconstrucción morfológica para mejor extracción de regiones
  2. OCR integrado:

    • Añadir reconocimiento de caracteres usando Tesseract
    • Validar detecciones mediante patrones alfanuméricos esperados
  3. Deep Learning híbrido:

    • Utilizar morfología como preprocesamiento para redes neuronales (YOLO, Faster R-CNN)
    • Comparar rendimiento: morfología clásica vs. detección por CNN
  4. Optimización de rendimiento:

    • Implementación GPU con CUDA para operaciones morfológicas en tiempo real
    • Pipeline de procesamiento paralelo para múltiples imágenes
  5. Dataset expandido:

    • Incluir placas de diferentes países y formatos
    • Aumentación de datos con variaciones de iluminación, ruido y oclusión controlada

Reflexión Final¶

Este mini proyecto validó la efectividad de las operaciones morfológicas en aplicaciones reales de visión computacional. Aunque técnicas modernas de deep learning pueden superar el rendimiento en detección, la morfología matemática ofrece ventajas importantes:

  • Interpretabilidad: Cada operación tiene un significado geométrico claro
  • Eficiencia computacional: No requiere entrenamiento ni GPUs potentes
  • Control explícito: Los parámetros son ajustables y predecibles
  • Base sólida: Comprensión fundamental aplicable a cualquier framework moderno

La experiencia de calibración manual de kernels y umbrales proporcionó intuición valiosa sobre el comportamiento de operadores morfológicos que trasciende cualquier implementación específica.


Referencias¶

Gonzalez, R. C., & Woods, R. E. (2018). Digital image processing (4th ed.). Pearson.

Haralick, R. M., Sternberg, S. R., & Zhuang, X. (1987). Image analysis using mathematical morphology. IEEE Transactions on Pattern Analysis and Machine Intelligence, 9(4), 532-550. https://doi.org/10.1109/TPAMI.1987.4767941

OpenCV. (2024). Morphological transformations. OpenCV Documentation. https://docs.opencv.org/4.x/d9/d61/tutorial_py_morphological_ops.html

Serra, J. (1982). Image analysis and mathematical morphology. Academic Press.

Soille, P. (2003). Morphological image analysis: Principles and applications (2nd ed.). Springer-Verlag. https://doi.org/10.1007/978-3-662-05088-0

Sonka, M., Hlavac, V., & Boyle, R. (2014). Image processing, analysis, and machine vision (4th ed.). Cengage Learning.

Vincent, L. (1993). Morphological grayscale reconstruction in image analysis: Applications and efficient algorithms. IEEE Transactions on Image Processing, 2(2), 176-201. https://doi.org/10.1109/83.217222


Tecnológico de Monterrey
Maestría en Inteligencia Artificial Aplicada
Visión Computacional para Imágenes y Video
Team 13 - Octubre 2025